Commit bfd90f8b authored by Vladimir Shushlin's avatar Vladimir Shushlin

Refactor pages migration services

Got rid of exceptions used for flow control
parent 0a62ff7c
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
module Pages module Pages
class MigrateLegacyStorageToDeploymentService class MigrateLegacyStorageToDeploymentService
ExclusiveLeaseTakenError = Class.new(StandardError) ExclusiveLeaseTakenError = Class.new(StandardError)
FailedToCreateArchiveError = Class.new(StandardError)
include BaseServiceUtility
include ::Pages::LegacyStorageLease include ::Pages::LegacyStorageLease
attr_reader :project attr_reader :project
...@@ -14,37 +14,43 @@ module Pages ...@@ -14,37 +14,43 @@ module Pages
end end
def execute def execute
migrated = try_obtain_lease do result = try_obtain_lease do
execute_unsafe execute_unsafe
true
end end
raise ExclusiveLeaseTakenError, "Can't migrate pages for project #{project.id}: exclusive lease taken" unless migrated raise ExclusiveLeaseTakenError, "Can't migrate pages for project #{project.id}: exclusive lease taken" if result.nil?
result
end end
private private
def execute_unsafe def execute_unsafe
archive_path, entries_count = ::Pages::ZipDirectoryService.new(project.pages_path).execute zip_result = ::Pages::ZipDirectoryService.new(project.pages_path).execute
if zip_result[:status] == :error
if !project.pages_metadatum&.reload&.pages_deployment &&
Feature.enabled?(:pages_migration_mark_as_not_deployed, project)
project.mark_pages_as_not_deployed
end
return error("Can't create zip archive: #{zip_result[:message]}")
end
archive_path = zip_result[:archive_path]
deployment = nil deployment = nil
File.open(archive_path) do |file| File.open(archive_path) do |file|
deployment = project.pages_deployments.create!( deployment = project.pages_deployments.create!(
file: file, file: file,
file_count: entries_count, file_count: zip_result[:entries_count],
file_sha256: Digest::SHA256.file(archive_path).hexdigest file_sha256: Digest::SHA256.file(archive_path).hexdigest
) )
end end
project.set_first_pages_deployment!(deployment) project.set_first_pages_deployment!(deployment)
rescue ::Pages::ZipDirectoryService::InvalidArchiveError => e
if !project.pages_metadatum&.reload&.pages_deployment &&
Feature.enabled?(:pages_migration_mark_as_not_deployed, project)
project.mark_pages_as_not_deployed
end
raise FailedToCreateArchiveError, e success
ensure ensure
FileUtils.rm_f(archive_path) if archive_path FileUtils.rm_f(archive_path) if archive_path
end end
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
module Pages module Pages
class ZipDirectoryService class ZipDirectoryService
include BaseServiceUtility
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
Error = Class.new(::StandardError) # used only to track exceptions in Sentry
InvalidArchiveError = Class.new(Error) InvalidEntryError = Class.new(StandardError)
InvalidEntryError = Class.new(Error)
PUBLIC_DIR = 'public' PUBLIC_DIR = 'public'
...@@ -15,19 +15,19 @@ module Pages ...@@ -15,19 +15,19 @@ module Pages
end end
def execute def execute
raise InvalidArchiveError, "Invalid work directory: #{@input_dir}" unless valid_work_directory? return error("Can not find valid public dir in #{@input_dir}") unless valid_path?(public_dir)
output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects
FileUtils.rm_f(output_file) FileUtils.rm_f(output_file)
count = 0 entries_count = 0
::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile| ::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile|
write_entry(zipfile, PUBLIC_DIR) write_entry(zipfile, PUBLIC_DIR)
count = zipfile.entries.count entries_count = zipfile.entries.count
end end
[output_file, count] success(archive_path: output_file, entries_count: entries_count)
rescue => e rescue => e
FileUtils.rm_f(output_file) if output_file FileUtils.rm_f(output_file) if output_file
raise e raise e
...@@ -39,9 +39,6 @@ module Pages ...@@ -39,9 +39,6 @@ module Pages
disk_file_path = File.join(real_dir, zipfile_path) disk_file_path = File.join(real_dir, zipfile_path)
unless valid_path?(disk_file_path) unless valid_path?(disk_file_path)
# archive without public directory is completelly unusable
raise InvalidArchiveError, "Invalid public directory: #{disk_file_path}" if zipfile_path == PUBLIC_DIR
# archive with invalid entry will just have this entry missing # archive with invalid entry will just have this entry missing
raise InvalidEntryError raise InvalidEntryError
end end
...@@ -80,8 +77,7 @@ module Pages ...@@ -80,8 +77,7 @@ module Pages
def valid_path?(disk_file_path) def valid_path?(disk_file_path)
realpath = File.realpath(disk_file_path) realpath = File.realpath(disk_file_path)
realpath == File.join(real_dir, PUBLIC_DIR) || realpath == public_dir || realpath.start_with?(public_dir + "/")
realpath.start_with?(File.join(real_dir, PUBLIC_DIR + "/"))
# happens if target of symlink isn't there # happens if target of symlink isn't there
rescue => e rescue => e
Gitlab::ErrorTracking.track_exception(e, input_dir: real_dir, disk_file_path: disk_file_path) Gitlab::ErrorTracking.track_exception(e, input_dir: real_dir, disk_file_path: disk_file_path)
...@@ -89,18 +85,16 @@ module Pages ...@@ -89,18 +85,16 @@ module Pages
false false
end end
def valid_work_directory?
Dir.exist?(real_dir)
rescue => e
Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir)
false
end
def real_dir def real_dir
strong_memoize(:real_dir) do strong_memoize(:real_dir) do
File.realpath(@input_dir) rescue nil File.realpath(@input_dir) rescue nil
end end
end end
def public_dir
strong_memoize(:public_dir) do
File.join(real_dir, PUBLIC_DIR) rescue nil
end
end
end end
end end
...@@ -6,24 +6,29 @@ namespace :gitlab do ...@@ -6,24 +6,29 @@ namespace :gitlab do
task migrate_legacy_storage: :gitlab_environment do task migrate_legacy_storage: :gitlab_environment do
logger = Logger.new(STDOUT) logger = Logger.new(STDOUT)
logger.info('Starting to migrate legacy pages storage to zip deployments') logger.info('Starting to migrate legacy pages storage to zip deployments')
migrated_projects = 0 processed_projects = 0
ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: 10) do |batch| ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: 10) do |batch|
batch.preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]).each do |metadatum| batch.preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]).each do |metadatum|
project = metadatum.project project = metadatum.project
result = nil
time = Benchmark.realtime do time = Benchmark.realtime do
::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute
end end
processed_projects += 1
migrated_projects += 1 if result[:status] == :success
logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds") logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds")
else
logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time} seconds: #{result[:message]}")
end
rescue => e rescue => e
logger.error("#{e.message} project_id: #{project&.id}") logger.error("#{e.message} project_id: #{project&.id}")
Gitlab::ErrorTracking.track_exception(e, project_id: project&.id) Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
end end
logger.info("#{migrated_projects} pages projects are migrated") logger.info("#{processed_projects} pages projects are processed")
end end
end end
end end
......
...@@ -11,9 +11,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -11,9 +11,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
expect do expect(service.execute).to(
service.execute eq(status: :error,
end.to raise_error(described_class::FailedToCreateArchiveError) message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}")
)
expect(project.pages_metadatum.reload.deployed).to eq(false) expect(project.pages_metadatum.reload.deployed).to eq(false)
end end
...@@ -25,9 +26,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -25,9 +26,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
expect do expect(service.execute).to(
service.execute eq(status: :error,
end.to raise_error(described_class::FailedToCreateArchiveError) message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}")
)
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
end end
...@@ -39,9 +41,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -39,9 +41,10 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
expect do expect(service.execute).to(
service.execute eq(status: :error,
end.to raise_error(described_class::FailedToCreateArchiveError) message: "Can't create zip archive: Can not find valid public dir in #{project.pages_path}")
)
expect(project.pages_metadatum.reload.deployed).to eq(true) expect(project.pages_metadatum.reload.deployed).to eq(true)
end end
...@@ -49,7 +52,9 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -49,7 +52,9 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
it 'removes pages archive when can not save deployment' do it 'removes pages archive when can not save deployment' do
archive = fixture_file_upload("spec/fixtures/pages.zip") archive = fixture_file_upload("spec/fixtures/pages.zip")
expect_next_instance_of(::Pages::ZipDirectoryService) do |zip_service| expect_next_instance_of(::Pages::ZipDirectoryService) do |zip_service|
expect(zip_service).to receive(:execute).and_return([archive.path, 3]) expect(zip_service).to receive(:execute).and_return(status: :success,
archive_path: archive.path,
entries_count: 3)
end end
expect_next_instance_of(PagesDeployment) do |deployment| expect_next_instance_of(PagesDeployment) do |deployment|
...@@ -73,7 +78,7 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do ...@@ -73,7 +78,7 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
it 'creates pages deployment' do it 'creates pages deployment' do
expect do expect do
described_class.new(project).execute expect(described_class.new(project).execute).to eq(status: :success)
end.to change { project.reload.pages_deployments.count }.by(1) end.to change { project.reload.pages_deployments.count }.by(1)
deployment = project.pages_metadatum.pages_deployment deployment = project.pages_metadatum.pages_deployment
......
...@@ -3,13 +3,6 @@ ...@@ -3,13 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Pages::ZipDirectoryService do RSpec.describe Pages::ZipDirectoryService do
it 'raises error if project pages dir does not exist' do
expect do
described_class.new("/tmp/not/existing/dir").execute
end.to raise_error(described_class::InvalidArchiveError)
end
context 'when work dir exists' do
around do |example| around do |example|
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
@work_dir = dir @work_dir = dir
...@@ -21,21 +14,35 @@ RSpec.describe Pages::ZipDirectoryService do ...@@ -21,21 +14,35 @@ RSpec.describe Pages::ZipDirectoryService do
described_class.new(@work_dir).execute described_class.new(@work_dir).execute
end end
let(:archive) { result.first } let(:status) { result[:status] }
let(:entries_count) { result.second } let(:message) { result[:message] }
let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] }
it 'raises error if there is no public directory and does not leave archive' do it 'returns error if project pages dir does not exist' do
expect { archive }.to raise_error(described_class::InvalidArchiveError) expect(
described_class.new("/tmp/not/existing/dir").execute
).to eq(status: :error, message: "Can not find valid public dir in /tmp/not/existing/dir")
end
it 'returns nils if there is no public directory and does not leave archive' do
expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{@work_dir}")
expect(archive).to eq(nil)
expect(entries_count).to eq(nil)
expect(File.exist?(File.join(@work_dir, '@migrated.zip'))).to eq(false) expect(File.exist?(File.join(@work_dir, '@migrated.zip'))).to eq(false)
end end
it 'raises error if public directory is a symlink' do it 'returns nils if public directory is a symlink' do
create_dir('target') create_dir('target')
create_file('./target/index.html', 'hello') create_file('./target/index.html', 'hello')
create_link("public", "./target") create_link("public", "./target")
expect { archive }.to raise_error(described_class::InvalidArchiveError) expect(status).to eq(:error)
expect(message).to eq("Can not find valid public dir in #{@work_dir}")
expect(archive).to eq(nil)
expect(entries_count).to eq(nil)
end end
context 'when there is a public directory' do context 'when there is a public directory' do
...@@ -215,5 +222,4 @@ RSpec.describe Pages::ZipDirectoryService do ...@@ -215,5 +222,4 @@ RSpec.describe Pages::ZipDirectoryService do
yield zip_file yield zip_file
end end
end end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment