2020-03-11 11:09:37 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Gitlab
|
|
|
|
module BackgroundMigration
|
|
|
|
# Class that will fill the project_repositories table for projects that
|
|
|
|
# are on hashed storage and an entry is missing in this table.
|
|
|
|
class BackfillSnippetRepositories
|
|
|
|
MAX_RETRIES = 2
|
|
|
|
|
|
|
|
def perform(start_id, stop_id)
|
2020-05-14 20:08:06 -04:00
|
|
|
snippets = snippet_relation.where(id: start_id..stop_id)
|
|
|
|
|
|
|
|
migrate_snippets(snippets)
|
|
|
|
end
|
|
|
|
|
|
|
|
def perform_by_ids(snippet_ids)
|
|
|
|
snippets = snippet_relation.where(id: snippet_ids)
|
|
|
|
|
|
|
|
migrate_snippets(snippets)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def migrate_snippets(snippets)
|
|
|
|
snippets.find_each do |snippet|
|
2020-03-11 11:09:37 -04:00
|
|
|
# We need to expire the exists? value for the cached method in case it was cached
|
|
|
|
snippet.repository.expire_exists_cache
|
|
|
|
|
|
|
|
next if repository_present?(snippet)
|
|
|
|
|
|
|
|
retry_index = 0
|
2020-05-05 14:09:43 -04:00
|
|
|
@invalid_path_error = false
|
2020-05-11 17:09:40 -04:00
|
|
|
@invalid_signature_error = false
|
2020-03-11 11:09:37 -04:00
|
|
|
|
|
|
|
begin
|
|
|
|
create_repository_and_files(snippet)
|
|
|
|
|
|
|
|
logger.info(message: 'Snippet Migration: repository created and migrated', snippet: snippet.id)
|
2021-04-26 08:09:44 -04:00
|
|
|
rescue StandardError => e
|
2020-05-05 14:09:43 -04:00
|
|
|
set_file_path_error(e)
|
2020-05-11 17:09:40 -04:00
|
|
|
set_signature_error(e)
|
2020-05-05 14:09:43 -04:00
|
|
|
|
2020-03-11 11:09:37 -04:00
|
|
|
retry_index += 1
|
|
|
|
|
2020-05-11 17:09:40 -04:00
|
|
|
retry if retry_index < max_retries
|
2020-03-11 11:09:37 -04:00
|
|
|
|
|
|
|
logger.error(message: "Snippet Migration: error migrating snippet. Reason: #{e.message}", snippet: snippet.id)
|
|
|
|
|
|
|
|
destroy_snippet_repository(snippet)
|
|
|
|
delete_repository(snippet)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-14 20:08:06 -04:00
|
|
|
def snippet_relation
|
|
|
|
@snippet_relation ||= Snippet.includes(:author, snippet_repository: :shard)
|
|
|
|
end
|
2020-03-11 11:09:37 -04:00
|
|
|
|
|
|
|
def repository_present?(snippet)
|
|
|
|
snippet.snippet_repository && !snippet.empty_repo?
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_repository_and_files(snippet)
|
|
|
|
snippet.create_repository
|
|
|
|
create_commit(snippet)
|
|
|
|
end
|
|
|
|
|
2020-04-28 17:09:35 -04:00
|
|
|
# Removing the db record
|
2020-03-11 11:09:37 -04:00
|
|
|
def destroy_snippet_repository(snippet)
|
2020-04-28 17:09:35 -04:00
|
|
|
snippet.snippet_repository&.delete
|
2021-04-26 08:09:44 -04:00
|
|
|
rescue StandardError => e
|
2020-03-11 11:09:37 -04:00
|
|
|
logger.error(message: "Snippet Migration: error destroying snippet repository. Reason: #{e.message}", snippet: snippet.id)
|
|
|
|
end
|
|
|
|
|
2020-04-28 17:09:35 -04:00
|
|
|
# Removing the repository in disk
|
2020-03-11 11:09:37 -04:00
|
|
|
def delete_repository(snippet)
|
2020-04-28 17:09:35 -04:00
|
|
|
return unless snippet.repository_exists?
|
|
|
|
|
|
|
|
snippet.repository.remove
|
|
|
|
snippet.repository.expire_exists_cache
|
2021-04-26 08:09:44 -04:00
|
|
|
rescue StandardError => e
|
2020-03-11 11:09:37 -04:00
|
|
|
logger.error(message: "Snippet Migration: error deleting repository. Reason: #{e.message}", snippet: snippet.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def logger
|
|
|
|
@logger ||= Gitlab::BackgroundMigration::Logger.build
|
|
|
|
end
|
|
|
|
|
|
|
|
def snippet_action(snippet)
|
|
|
|
# We don't need the previous_path param
|
|
|
|
# Because we're not updating any existing file
|
|
|
|
[{ file_path: filename(snippet),
|
|
|
|
content: snippet.content }]
|
|
|
|
end
|
|
|
|
|
|
|
|
def filename(snippet)
|
2020-05-05 14:09:43 -04:00
|
|
|
file_name = snippet.file_name
|
|
|
|
file_name = file_name.parameterize if @invalid_path_error
|
|
|
|
|
|
|
|
file_name.presence || empty_file_name
|
2020-03-11 11:09:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def empty_file_name
|
|
|
|
@empty_file_name ||= "#{SnippetRepository::DEFAULT_EMPTY_FILE_NAME}1.txt"
|
|
|
|
end
|
|
|
|
|
|
|
|
def commit_attrs
|
2021-07-20 20:09:07 -04:00
|
|
|
@commit_attrs ||= { branch_name: 'main', message: 'Initial commit' }
|
2020-03-11 11:09:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def create_commit(snippet)
|
2020-09-28 08:10:02 -04:00
|
|
|
snippet.snippet_repository.multi_files_action(commit_author(snippet), snippet_action(snippet), **commit_attrs)
|
2020-04-28 17:09:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# If the user is not allowed to access git or update the snippet
|
|
|
|
# because it is blocked, internal, ghost, ... we cannot commit
|
|
|
|
# files because these users are not allowed to, but we need to
|
|
|
|
# migrate their snippets as well.
|
2020-05-06 14:09:38 -04:00
|
|
|
# In this scenario the migration bot user will be the one that will commit the files.
|
2020-04-28 17:09:35 -04:00
|
|
|
def commit_author(snippet)
|
2020-05-11 08:10:28 -04:00
|
|
|
return migration_bot_user if snippet_content_size_over_limit?(snippet)
|
2020-05-11 17:09:40 -04:00
|
|
|
return migration_bot_user if @invalid_signature_error
|
2020-05-11 08:10:28 -04:00
|
|
|
|
2020-04-28 17:09:35 -04:00
|
|
|
if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet)
|
|
|
|
snippet.author
|
|
|
|
else
|
2020-05-06 14:09:38 -04:00
|
|
|
migration_bot_user
|
2020-04-28 17:09:35 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-06 14:09:38 -04:00
|
|
|
def migration_bot_user
|
|
|
|
@migration_bot_user ||= User.migration_bot
|
2020-03-11 11:09:37 -04:00
|
|
|
end
|
2020-05-05 14:09:43 -04:00
|
|
|
|
|
|
|
# We sometimes receive invalid path errors from Gitaly if the Snippet filename
|
|
|
|
# cannot be parsed into a valid git path.
|
|
|
|
# In this situation, we need to parameterize the file name of the Snippet so that
|
|
|
|
# the migration can succeed, to achieve that, we'll identify in migration retries
|
|
|
|
# that the path is invalid
|
|
|
|
def set_file_path_error(error)
|
2020-05-11 17:09:40 -04:00
|
|
|
@invalid_path_error ||= error.is_a?(SnippetRepository::InvalidPathError)
|
|
|
|
end
|
|
|
|
|
|
|
|
# We sometimes receive invalid signature from Gitaly if the commit author
|
|
|
|
# name or email is invalid to create the commit signature.
|
|
|
|
# In this situation, we set the error and use the migration_bot since
|
|
|
|
# the information used to build it is valid
|
|
|
|
def set_signature_error(error)
|
|
|
|
@invalid_signature_error ||= error.is_a?(SnippetRepository::InvalidSignatureError)
|
|
|
|
end
|
|
|
|
|
|
|
|
# In the case where the snippet file_name is invalid and also the
|
|
|
|
# snippet author has invalid commit info, we need to increase the
|
|
|
|
# number of retries by 1, because we will receive two errors
|
|
|
|
# from Gitaly and, in the third one, we will commit successfully.
|
|
|
|
def max_retries
|
|
|
|
MAX_RETRIES + (@invalid_signature_error && @invalid_path_error ? 1 : 0)
|
2020-05-05 14:09:43 -04:00
|
|
|
end
|
2020-05-11 08:10:28 -04:00
|
|
|
|
|
|
|
def snippet_content_size_over_limit?(snippet)
|
|
|
|
snippet.content.size > Gitlab::CurrentSettings.snippet_size_limit
|
|
|
|
end
|
2020-03-11 11:09:37 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|