gitlab-org--gitlab-foss/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_proj...

431 lines
16 KiB
Ruby

# frozen_string_literal: true
# Lifesize project import test executed from https://gitlab.com/gitlab-org/manage/import/import-metrics
# rubocop:disable Rails/Pluck, Layout/LineLength, RSpec/MultipleMemoizedHelpers
module QA
RSpec.describe "Manage", requires_admin: 'creates users', only: { job: 'large-gitlab-import' } do
describe "Gitlab migration", product_group: :import do
let(:logger) { Runtime::Logger.logger }
let(:differ) { RSpec::Support::Differ.new(color: true) }
let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' }
let(:gitlab_project) { ENV['QA_LARGE_IMPORT_REPO'] || 'dri' }
let(:gitlab_source_address) { ENV['QA_LARGE_IMPORT_SOURCE_URL'] || 'https://staging.gitlab.com' }
let(:import_wait_duration) do
{
max_duration: (ENV['QA_LARGE_IMPORT_DURATION'] || 3600).to_i,
sleep_interval: 30
}
end
let(:admin_api_client) { Runtime::API::Client.as_admin }
# explicitly create PAT via api to not create it via UI in environments where admin token env var is not present
let(:target_api_client) do
Runtime::API::Client.new(
user: user,
personal_access_token: Resource::PersonalAccessToken.fabricate_via_api! do |pat|
pat.api_client = admin_api_client
end.token
)
end
let(:user) do
Resource::User.fabricate_via_api! do |usr|
usr.api_client = admin_api_client
end
end
let(:source_api_client) do
Runtime::API::Client.new(
gitlab_source_address,
personal_access_token: ENV["QA_LARGE_IMPORT_GL_TOKEN"],
is_new_session: false
)
end
let(:sandbox) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = admin_api_client
end
end
let(:destination_group) do
Resource::Group.fabricate_via_api! do |group|
group.api_client = admin_api_client
group.sandbox = sandbox
group.path = "imported-group-destination-#{SecureRandom.hex(4)}"
end
end
# Source group and it's objects
#
let(:source_group) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = source_api_client
group.path = gitlab_group
end
end
let(:source_project) { source_group.projects.find { |project| project.name.include?(gitlab_project) }.reload! }
let(:source_branches) { source_project.repository_branches(auto_paginate: true).map { |b| b[:name] } }
let(:source_commits) { source_project.commits(auto_paginate: true).map { |c| c[:id] } }
let(:source_labels) { source_project.labels(auto_paginate: true).map { |l| l.except(:id) } }
let(:source_milestones) { source_project.milestones(auto_paginate: true).map { |ms| ms.except(:id, :web_url, :project_id) } }
let(:source_pipelines) { source_project.pipelines(auto_paginate: true).map { |pp| pp.except(:id, :web_url, :project_id) } }
let(:source_mrs) { fetch_mrs(source_project, source_api_client) }
let(:source_issues) { fetch_issues(source_project, source_api_client) }
# Imported group and it's objects
#
let(:imported_group) do
Resource::BulkImportGroup.fabricate_via_api! do |group|
group.import_access_token = source_api_client.personal_access_token # token for importing on source instance
group.api_client = target_api_client # token used by qa framework to access resources in destination instance
group.gitlab_address = gitlab_source_address
group.source_group = source_group
group.sandbox = destination_group
end
end
let(:imported_project) { imported_group.projects.find { |project| project.name.include?(gitlab_project) }.reload! }
let(:branches) { imported_project.repository_branches(auto_paginate: true).map { |b| b[:name] } }
let(:commits) { imported_project.commits(auto_paginate: true).map { |c| c[:id] } }
let(:labels) { imported_project.labels(auto_paginate: true).map { |l| l.except(:id) } }
let(:milestones) { imported_project.milestones(auto_paginate: true).map { |ms| ms.except(:id, :web_url, :project_id) } }
let(:pipelines) { imported_project.pipelines.map { |pp| pp.except(:id, :web_url, :project_id) } }
let(:mrs) { fetch_mrs(imported_project, target_api_client) }
let(:issues) { fetch_issues(imported_project, target_api_client) }
let(:import_failures) { imported_group.import_details.sum([]) { |details| details[:failures] } }
before do
destination_group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
# rubocop:disable RSpec/InstanceVariable
after do |example|
# Log failures for easier debugging
Runtime::Logger.error("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
next unless defined?(@import_time)
# save data for comparison notification creation
save_json(
"data",
{
importer: :gitlab,
import_time: @import_time,
errors: import_failures,
source: {
name: "GitLab Source",
project_name: source_project.path_with_namespace,
address: gitlab_source_address,
data: {
branches: source_branches.length,
commits: source_commits.length,
labels: source_labels.length,
milestones: source_milestones.length,
pipelines: source_pipelines.length,
mrs: source_mrs.length,
mr_comments: source_mrs.sum { |_k, v| v[:comments].length },
issues: source_issues.length,
issue_comments: source_issues.sum { |_k, v| v[:comments].length }
}
},
target: {
name: "GitLab Target",
project_name: imported_project.path_with_namespace,
address: QA::Runtime::Scenario.gitlab_address,
data: {
branches: branches.length,
commits: commits.length,
labels: labels.length,
milestones: milestones.length,
pipelines: pipelines.length,
mrs: mrs.length,
mr_comments: mrs.sum { |_k, v| v[:comments].length },
issues: issues.length,
issue_comments: issues.sum { |_k, v| v[:comments].length }
}
},
diff: {
mrs: @mr_diff,
issues: @issue_diff
}
}
)
end
# rubocop:enable RSpec/InstanceVariable
it "migrates large gitlab group via api", testcase: "https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/358842" do
start = Time.now
# trigger import and log imported group path
logger.info("== Importing group '#{gitlab_group}' in to '#{imported_group.full_path}' ==")
# fetch all objects right after import has started
fetch_source_gitlab_objects
# wait for import to finish and save import time
logger.info("== Waiting for import to be finished ==")
expect { imported_group.import_status }.not_to eventually_eq("started").within(import_wait_duration)
# finished status actually means success, don't wait for finished status explicitly
# because test would wait full duration if returned status is "failed"
expect(imported_group.import_status).to eq("finished")
@import_time = Time.now - start
aggregate_failures do
verify_repository_import
verify_labels_import
verify_milestones_import
verify_pipelines_import
verify_merge_requests_import
verify_issues_import
end
end
# Fetch source project objects for comparison
#
# @return [void]
def fetch_source_gitlab_objects
logger.info("== Fetching source group objects ==")
source_branches
source_commits
source_labels
source_milestones
source_pipelines
source_mrs
source_issues
end
# Verify repository imported correctly
#
# @return [void]
def verify_repository_import
logger.info("== Verifying repository import ==")
expect(imported_project.description).to eq(source_project.description)
expect(branches).to match_array(source_branches)
expect(commits).to match_array(source_commits)
end
# Verify imported labels
#
# @return [void]
def verify_labels_import
logger.info("== Verifying label import ==")
expect(labels).to include(*source_labels)
end
# Verify milestones import
#
# @return [void]
def verify_milestones_import
logger.info("== Verifying milestones import ==")
expect(milestones).to match_array(source_milestones)
end
# Verify pipelines import
#
# @return [void]
def verify_pipelines_import
logger.info("== Verifying pipelines import ==")
expect(pipelines).to match_array(source_pipelines)
end
# Verify imported merge requests and mr issues
#
# @return [void]
def verify_merge_requests_import
logger.info("== Verifying merge request import ==")
@mr_diff = verify_mrs_or_issues('mr')
end
# Verify imported issues and issue comments
#
# @return [void]
def verify_issues_import
logger.info("== Verifying issue import ==")
@issue_diff = verify_mrs_or_issues('issue')
end
# Verify imported mrs or issues and return missing items
#
# @param [String] type verification object, 'mr' or 'issue'
# @return [Hash]
def verify_mrs_or_issues(type)
# Compare length to have easy to read overview how many objects are missing
#
expected = type == 'mr' ? source_mrs : source_issues
actual = type == 'mr' ? mrs : issues
count_msg = "Expected to contain same amount of #{type}s. Source: #{expected.length}, Target: #{actual.length}"
expect(actual.length).to eq(expected.length), count_msg
comment_diff = verify_comments(type, actual, expected)
{
"missing_#{type}s": (expected.keys - actual.keys).map { |it| actual[it]&.slice(:title, :url) }.compact,
"extra_#{type}s": (actual.keys - expected.keys).map { |it| expected[it]&.slice(:title, :url) }.compact,
"#{type}_comments": comment_diff
}
end
# Verify imported comments
#
# @param [String] type verification object, 'mrs' or 'issues'
# @param [Hash] actual
# @param [Hash] expected
# @return [Hash]
def verify_comments(type, actual, expected)
actual.each_with_object([]) do |(key, actual_item), diff|
expected_item = expected[key]
title = actual_item[:title]
msg = "expected #{type} with title '#{title}' to have"
# Print title in the error message to see which object is missing
#
expect(actual_item).to be_truthy, "#{msg} been imported"
next unless expected_item
# Print difference in the description
#
expected_body = expected_item[:body]
actual_body = actual_item[:body]
body_msg = "#{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)}"
expect(actual_body).to eq(expected_body), body_msg
# Print difference in state
#
expected_state = expected_item[:state]
actual_state = actual_item[:state]
state_msg = "#{msg} same state. Source: #{expected_state}, Target: #{actual_state}"
expect(actual_state).to eq(expected_state), state_msg
# Print amount difference first
#
expected_comments = expected_item[:comments]
actual_comments = actual_item[:comments]
comment_count_msg = <<~MSG
#{msg} same amount of comments. Source: #{expected_comments.length}, Target: #{actual_comments.length}
MSG
expect(actual_comments.length).to eq(expected_comments.length), comment_count_msg
expect(actual_comments).to match_array(expected_comments)
# Save comment diff
#
missing_comments = expected_comments - actual_comments
extra_comments = actual_comments - expected_comments
next if missing_comments.empty? && extra_comments.empty?
diff << {
title: title,
target_url: actual_item[:url],
source_url: expected_item[:url],
missing_comments: missing_comments,
extra_comments: extra_comments
}
end
end
private
# Project merge requests with comments
#
# @param [QA::Resource::Project]
# @param [Runtime::API::Client] client
# @return [Hash]
def fetch_mrs(project, client)
imported_mrs = project.merge_requests(auto_paginate: true, attempts: 2)
Parallel.map(imported_mrs, in_threads: 4) do |mr|
resource = Resource::MergeRequest.init do |resource|
resource.project = project
resource.iid = mr[:iid]
resource.api_client = client
end
[mr[:iid], {
url: mr[:web_url],
title: mr[:title],
body: sanitize_description(mr[:description]) || '',
state: mr[:state],
comments: resource
.comments(auto_paginate: true, attempts: 2)
.map { |c| sanitize_comment(c[:body]) }
}]
end.to_h
end
# Project issues with comments
#
# @param [QA::Resource::Project]
# @param [Runtime::API::Client] client
# @return [Hash]
def fetch_issues(project, client)
imported_issues = project.issues(auto_paginate: true, attempts: 2)
Parallel.map(imported_issues, in_threads: 4) do |issue|
resource = Resource::Issue.init do |issue_resource|
issue_resource.project = project
issue_resource.iid = issue[:iid]
issue_resource.api_client = client
end
[issue[:iid], {
url: issue[:web_url],
title: issue[:title],
state: issue[:state],
body: sanitize_description(issue[:description]) || '',
comments: resource
.comments(auto_paginate: true, attempts: 2)
.map { |c| sanitize_comment(c[:body]) }
}]
end.to_h
end
# Importer user mention pattern
#
# @return [Regex]
def created_by_pattern
@created_by_pattern ||= /\n\n \*By #{importer_username_pattern} on \S+ \(imported from GitLab\)\*/
end
# Username of importer user for removal from comments and descriptions
#
# @return [String]
def importer_username_pattern
@importer_username_pattern ||= ENV['QA_LARGE_IMPORT_USER_PATTERN'] || "(gitlab-migration|GitLab QA Bot)"
end
# Remove added prefixes from comments
#
# @param [String] body
# @return [String]
def sanitize_comment(body)
body&.gsub(created_by_pattern, "")
end
# Remove created by prefix from descripion
#
# @param [String] body
# @return [String]
def sanitize_description(body)
body&.gsub(created_by_pattern, "")
end
# Save json as file
#
# @param [String] name
# @param [Hash] json
# @return [void]
def save_json(name, json)
File.open("tmp/#{name}.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
end
end
end
end
# rubocop:enable Rails/Pluck, Layout/LineLength, RSpec/MultipleMemoizedHelpers