Commit c571bcca authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '295206-follow-up-from-add-pages-migration-service' into 'master'

Refactor pages migration services

See merge request gitlab-org/gitlab!50624
parents 5824d80f bfd90f8b
...@@ -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,217 +3,223 @@ ...@@ -3,217 +3,223 @@
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 around do |example|
expect do Dir.mktmpdir do |dir|
described_class.new("/tmp/not/existing/dir").execute @work_dir = dir
end.to raise_error(described_class::InvalidArchiveError) example.run
end
end end
context 'when work dir exists' do let(:result) do
around do |example| described_class.new(@work_dir).execute
Dir.mktmpdir do |dir| end
@work_dir = dir
example.run
end
end
let(:result) do let(:status) { result[:status] }
described_class.new(@work_dir).execute let(:message) { result[:message] }
end let(:archive) { result[:archive_path] }
let(:entries_count) { result[:entries_count] }
let(:archive) { result.first } it 'returns error if project pages dir does not exist' do
let(:entries_count) { result.second } 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 'raises error if there is no public directory and does not leave archive' do it 'returns nils if there is no public directory and does not leave archive' do
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)
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)
end 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
context 'when there is a public directory' do context 'when there is a public directory' do
before do before do
create_dir('public') create_dir('public')
end end
it 'creates the file next the public directory' do it 'creates the file next the public directory' do
expect(archive).to eq(File.join(@work_dir, "@migrated.zip")) expect(archive).to eq(File.join(@work_dir, "@migrated.zip"))
end end
it 'includes public directory' do it 'includes public directory' do
with_zip_file do |zip_file| with_zip_file do |zip_file|
entry = zip_file.get_entry("public/") entry = zip_file.get_entry("public/")
expect(entry.ftype).to eq(:directory) expect(entry.ftype).to eq(:directory)
end
end end
end
it 'returns number of entries' do it 'returns number of entries' do
create_file("public/index.html", "hello") create_file("public/index.html", "hello")
create_link("public/link.html", "./index.html") create_link("public/link.html", "./index.html")
expect(entries_count).to eq(3) # + 'public' directory expect(entries_count).to eq(3) # + 'public' directory
end end
it 'removes the old file if it exists' do it 'removes the old file if it exists' do
# simulate the old run # simulate the old run
described_class.new(@work_dir).execute described_class.new(@work_dir).execute
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect(zip_file.entries.count).to eq(1) expect(zip_file.entries.count).to eq(1)
end
end end
end
it 'ignores other top level files and directories' do it 'ignores other top level files and directories' do
create_file("top_level.html", "hello") create_file("top_level.html", "hello")
create_dir("public2") create_dir("public2")
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect { zip_file.get_entry("top_level.html") }.to raise_error(Errno::ENOENT) expect { zip_file.get_entry("top_level.html") }.to raise_error(Errno::ENOENT)
expect { zip_file.get_entry("public2/") }.to raise_error(Errno::ENOENT) expect { zip_file.get_entry("public2/") }.to raise_error(Errno::ENOENT)
end
end end
end
it 'includes index.html file' do it 'includes index.html file' do
create_file("public/index.html", "Hello!") create_file("public/index.html", "Hello!")
with_zip_file do |zip_file| with_zip_file do |zip_file|
entry = zip_file.get_entry("public/index.html") entry = zip_file.get_entry("public/index.html")
expect(zip_file.read(entry)).to eq("Hello!") expect(zip_file.read(entry)).to eq("Hello!")
end
end end
end
it 'includes hidden file' do it 'includes hidden file' do
create_file("public/.hidden.html", "Hello!") create_file("public/.hidden.html", "Hello!")
with_zip_file do |zip_file| with_zip_file do |zip_file|
entry = zip_file.get_entry("public/.hidden.html") entry = zip_file.get_entry("public/.hidden.html")
expect(zip_file.read(entry)).to eq("Hello!") expect(zip_file.read(entry)).to eq("Hello!")
end
end end
end
it 'includes nested directories and files' do it 'includes nested directories and files' do
create_dir("public/nested") create_dir("public/nested")
create_dir("public/nested/nested2") create_dir("public/nested/nested2")
create_file("public/nested/nested2/nested.html", "Hello nested") create_file("public/nested/nested2/nested.html", "Hello nested")
with_zip_file do |zip_file| with_zip_file do |zip_file|
entry = zip_file.get_entry("public/nested") entry = zip_file.get_entry("public/nested")
expect(entry.ftype).to eq(:directory) expect(entry.ftype).to eq(:directory)
entry = zip_file.get_entry("public/nested/nested2") entry = zip_file.get_entry("public/nested/nested2")
expect(entry.ftype).to eq(:directory) expect(entry.ftype).to eq(:directory)
entry = zip_file.get_entry("public/nested/nested2/nested.html") entry = zip_file.get_entry("public/nested/nested2/nested.html")
expect(zip_file.read(entry)).to eq("Hello nested") expect(zip_file.read(entry)).to eq("Hello nested")
end
end end
end
it 'adds a valid symlink' do it 'adds a valid symlink' do
create_file("public/target.html", "hello") create_file("public/target.html", "hello")
create_link("public/link.html", "./target.html") create_link("public/link.html", "./target.html")
with_zip_file do |zip_file| with_zip_file do |zip_file|
entry = zip_file.get_entry("public/link.html") entry = zip_file.get_entry("public/link.html")
expect(entry.ftype).to eq(:symlink) expect(entry.ftype).to eq(:symlink)
expect(zip_file.read(entry)).to eq("./target.html") expect(zip_file.read(entry)).to eq("./target.html")
end
end end
end
it 'ignores the symlink pointing outside of public directory' do it 'ignores the symlink pointing outside of public directory' do
create_file("target.html", "hello") create_file("target.html", "hello")
create_link("public/link.html", "../target.html") create_link("public/link.html", "../target.html")
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT) expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
end
end end
end
it 'ignores the symlink if target is absent' do it 'ignores the symlink if target is absent' do
create_link("public/link.html", "./target.html") create_link("public/link.html", "./target.html")
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT) expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
end
end end
end
it 'ignores symlink if is absolute and points to outside of directory' do it 'ignores symlink if is absolute and points to outside of directory' do
target = File.join(@work_dir, "target") target = File.join(@work_dir, "target")
FileUtils.touch(target) FileUtils.touch(target)
create_link("public/link.html", target) create_link("public/link.html", target)
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT) expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
end
end end
end
it "includes raw symlink if it's target is a valid directory" do it "includes raw symlink if it's target is a valid directory" do
create_dir("public/target") create_dir("public/target")
create_file("public/target/index.html", "hello") create_file("public/target/index.html", "hello")
create_link("public/link", "./target") create_link("public/link", "./target")
with_zip_file do |zip_file| with_zip_file do |zip_file|
expect(zip_file.entries.count).to eq(4) # /public and 3 created above expect(zip_file.entries.count).to eq(4) # /public and 3 created above
entry = zip_file.get_entry("public/link") entry = zip_file.get_entry("public/link")
expect(entry.ftype).to eq(:symlink) expect(entry.ftype).to eq(:symlink)
expect(zip_file.read(entry)).to eq("./target") expect(zip_file.read(entry)).to eq("./target")
end
end end
end end
end
context "validating fixtures pages archives" do context "validating fixtures pages archives" do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:fixture_path) do where(:fixture_path) do
["spec/fixtures/pages.zip", "spec/fixtures/pages_non_writeable.zip"] ["spec/fixtures/pages.zip", "spec/fixtures/pages_non_writeable.zip"]
end end
with_them do with_them do
let(:full_fixture_path) { Rails.root.join(fixture_path) } let(:full_fixture_path) { Rails.root.join(fixture_path) }
it 'a created archives contains exactly the same entries' do it 'a created archives contains exactly the same entries' do
SafeZip::Extract.new(full_fixture_path).extract(directories: ['public'], to: @work_dir) SafeZip::Extract.new(full_fixture_path).extract(directories: ['public'], to: @work_dir)
with_zip_file do |created_archive| with_zip_file do |created_archive|
Zip::File.open(full_fixture_path) do |original_archive| Zip::File.open(full_fixture_path) do |original_archive|
original_archive.entries do |original_entry| original_archive.entries do |original_entry|
created_entry = created_archive.get_entry(original_entry.name) created_entry = created_archive.get_entry(original_entry.name)
expect(created_entry.name).to eq(original_entry.name) expect(created_entry.name).to eq(original_entry.name)
expect(created_entry.ftype).to eq(original_entry.ftype) expect(created_entry.ftype).to eq(original_entry.ftype)
expect(created_archive.read(created_entry)).to eq(original_archive.read(original_entry)) expect(created_archive.read(created_entry)).to eq(original_archive.read(original_entry))
end
end end
end end
end end
end end
end end
end
def create_file(name, content) def create_file(name, content)
File.open(File.join(@work_dir, name), "w") do |f| File.open(File.join(@work_dir, name), "w") do |f|
f.write(content) f.write(content)
end
end end
end
def create_dir(dir) def create_dir(dir)
Dir.mkdir(File.join(@work_dir, dir)) Dir.mkdir(File.join(@work_dir, dir))
end end
def create_link(new_name, target) def create_link(new_name, target)
File.symlink(target, File.join(@work_dir, new_name)) File.symlink(target, File.join(@work_dir, new_name))
end end
def with_zip_file def with_zip_file
Zip::File.open(archive) do |zip_file| Zip::File.open(archive) do |zip_file|
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