# frozen_string_literal: true
# rubocop:disable Metrics/AbcSize
require 'securerandom'
require 'net/http'
require 'uri'
require 'forwardable'
require 'openssl'
require 'tempfile'
require 'json'
module Gitlab
module QA
module Component
class Gitlab < Base
extend Forwardable
using Rainbow
attr_reader :release, :omnibus_configuration, :omnibus_gitlab_rails_env
attr_accessor :tls, :skip_availability_check, :runner_network, :seed_admin_token, :seed_db, :skip_server_hooks
attr_writer :name, :relative_path
def_delegators :release, :tag, :image, :edition
CERTIFICATES_PATH = File.expand_path('../../../../tls_certificates', __dir__)
DATA_SEED_PATH = File.expand_path('../../../../support/data', __dir__)
SSL_PATH = '/etc/gitlab/ssl'
TRUSTED_PATH = '/etc/gitlab/trusted-certs'
DATA_PATH = '/tmp/data-seeds'
def initialize
super
@skip_availability_check = false
@omnibus_gitlab_rails_env = {}
@omnibus_configuration = Runtime::OmnibusConfiguration.new(Runtime::Scenario.omnibus_configuration)
@working_dir_tmp_cert_path = Dir.mktmpdir('certs', FileUtils.mkdir_p("#{Dir.pwd}/tmp"))
@authority_cert_path = "#{@working_dir_tmp_cert_path}/authority"
@gitlab_cert_path = "#{@working_dir_tmp_cert_path}/gitlab"
@gitaly_cert_path = "#{@working_dir_tmp_cert_path}/gitaly"
@volumes[@gitlab_cert_path] = SSL_PATH
@volumes[@authority_cert_path] = TRUSTED_PATH
@seed_admin_token = Runtime::Scenario.seed_admin_token
@seed_db = Runtime::Scenario.seed_db
@skip_server_hooks = Runtime::Scenario.skip_server_hooks
self.release = 'CE'
end
def set_formless_login_token
return if Runtime::Env.gitlab_qa_formless_login_token.to_s.strip.empty?
@omnibus_gitlab_rails_env['GITLAB_QA_FORMLESS_LOGIN_TOKEN'] = Runtime::Env.gitlab_qa_formless_login_token
end
def set_license_mode
return unless Runtime::Env.test_license_mode?
@omnibus_gitlab_rails_env['GITLAB_LICENSE_MODE'] = 'test'
@omnibus_gitlab_rails_env['CUSTOMER_PORTAL_URL'] = 'https://customers.staging.gitlab.com'
end
def elastic_url=(url)
@environment['ELASTIC_URL'] = url
end
def release=(release)
@release = QA::Release.new(release)
end
def name
@name ||= "gitlab-#{edition}-#{SecureRandom.hex(4)}"
end
def address
"#{scheme}://#{hostname}#{relative_path}"
end
def scheme
tls ? 'https' : 'http'
end
def gitlab_port
tls ? ["443:443"] : ["80"]
end
def gitaly_tls
@volumes.delete(@gitlab_cert_path)
@volumes[@gitaly_cert_path] = SSL_PATH
end
def relative_path
@relative_path ||= ''
end
def set_accept_insecure_certs
Runtime::Env.accept_insecure_certs = 'true'
end
def prepare
prepare_gitlab_omnibus_config
copy_certificates
super
end
def teardown!
FileUtils.rm_rf(@working_dir_tmp_cert_path)
super
end
def pull
docker.login(**release.login_params) if release.login_params
super
end
def exist?(image, tag)
docker.manifest_exists?("#{image}:#{tag}")
end
def prepare_gitlab_omnibus_config
set_formless_login_token
set_license_mode
return if omnibus_gitlab_rails_env.empty?
@omnibus_configuration << "gitlab_rails['env'] = #{@omnibus_gitlab_rails_env}"
end
def start # rubocop:disable Metrics/AbcSize
ensure_configured!
docker.run(image: image, tag: tag) do |command|
command << "-d"
command << "--name #{name}"
command << "--net #{network}"
command << "--hostname #{hostname}"
[*@ports, *gitlab_port].each do |mapping|
command.port(mapping)
end
@volumes.to_h.each do |to, from|
command.volume(to, from, 'Z')
end
command.volume(File.join(Runtime::Env.host_artifacts_dir, name, 'logs'), '/var/log/gitlab', 'Z')
@environment.to_h.each do |key, value|
command.env(key, value)
end
@network_aliases.to_a.each do |network_alias|
command << "--network-alias #{network_alias}"
end
end
return unless runner_network
Docker::Command.execute(
"network connect --alias #{name}.#{network} --alias #{name}.#{runner_network} #{runner_network} #{name}"
)
end
def reconfigure
setup_omnibus
@docker.attach(name) do |line, wait|
# TODO, workaround which allows to detach from the container
break if /gitlab Reconfigured!/.match?(line)
end
end
def wait_until_ready
return if skip_availability_check
availability = Availability.new(
name,
relative_path: relative_path,
scheme: scheme,
protocol_port: gitlab_port.first.to_i
)
Runtime::Logger.info("Waiting for GitLab to become healthy ...")
if availability.check(Runtime::Env.gitlab_availability_timeout)
Runtime::Logger.info("-> GitLab is available at `#{availability.uri}`!".bright)
else
abort '-> GitLab unavailable!'.red
end
end
def process_exec_commands
@docker.copy(name, DATA_SEED_PATH, DATA_PATH) if seed_admin_token || seed_db
exec_commands << seed_admin_token_command if seed_admin_token
exec_commands << seed_test_data_command if seed_db
exec_commands << Runtime::Scenario.omnibus_exec_commands
exec_commands << add_git_server_hooks unless skip_server_hooks
commands = exec_commands.flatten.uniq
return if commands.empty?
Runtime::Logger.info("Running exec_commands...")
commands.each { |command| @docker.exec(name, command) }
end
def rails_version
manifest = JSON.parse(read_package_manifest)
{
sha: manifest['software']['gitlab-rails']['locked_version'],
source: manifest['software']['gitlab-rails']['locked_source']['git']
}
end
def package_version
manifest = JSON.parse(read_package_manifest)
manifest['software']['package-scripts']['locked_version']
end
def copy_key_file(env_key)
key_dir = ENV['CI_PROJECT_DIR'] || Dir.tmpdir
key_file = Tempfile.new(env_key.downcase, key_dir)
key_file.write(ENV.fetch(env_key))
key_file.close
File.chmod(0o744, key_file.path)
@volumes[key_file.path] = key_file.path
key_file.path
end
private
def read_package_manifest
@docker.read_file(
@release.image, @release.tag,
'/opt/gitlab/version-manifest.json'
)
end
# Copy certs to a temporary directory in current working directory.
# This is needed for docker-in-docker ci environments where mount points outside of build dir are not accessible
#
# @return [void]
def copy_certificates
FileUtils.cp_r("#{CERTIFICATES_PATH}/.", @working_dir_tmp_cert_path)
end
def ensure_configured!
raise 'Please configure an instance first!' unless [name, release, network].all?
end
def setup_omnibus
@docker.write_files(name) do |f|
f.write('/etc/gitlab/gitlab.rb', @omnibus_configuration.to_s)
end
end
def seed_test_data_command
cmd = []
Runtime::Scenario.seed_db.each do |file_patterns|
Dir["#{DATA_SEED_PATH}/#{file_patterns}"].map { |f| File.basename f }.each do |file|
cmd << "gitlab-rails runner #{DATA_PATH}/#{file}"
end
end
cmd.uniq
end
def seed_admin_token_command
["gitlab-rails runner #{DATA_PATH}/admin_access_token_seed.rb"]
end
def add_git_server_hooks
global_server_prereceive_hook = <<~SCRIPT
#!/usr/bin/env bash
if [[ \\\$GL_PROJECT_PATH =~ 'reject-prereceive' ]]; then
echo 'GL-HOOK-ERR: Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive'
exit 1
fi
SCRIPT
[
@docker.exec(name, 'mkdir -p /opt/gitlab/embedded/service/gitlab-shell/hooks/pre-receive.d'),
@docker.write_files(name) do |f|
f.write(
'/opt/gitlab/embedded/service/gitlab-shell/hooks/pre-receive.d/pre-receive.d',
global_server_prereceive_hook, false
)
end,
@docker.exec(name, 'chmod +x /opt/gitlab/embedded/service/gitlab-shell/hooks/pre-receive.d/*')
]
end
class Availability
def initialize(name, relative_path: '', scheme: 'http', protocol_port: 80)
@docker = Docker::Engine.new
@name = name
@scheme = scheme
@relative_path = relative_path
@protocol_port = protocol_port
end
def check(retries)
retries.times do
return true if service_available?
sleep 1
end
false
end
def uri
@uri ||= begin
port = docker.port(name, protocol_port).split(':').last
URI.join("#{scheme}://#{docker.hostname}:#{port}", relative_path)
end
end
private
attr_reader :docker, :name, :relative_path, :scheme, :protocol_port
def service_available?
output = docker.inspect(name) { |command| command << "--format='{{json .State.Health.Status}}'" }
output == '"healthy"'
rescue Docker::Shellout::StatusError
false
end
end
end
end
end
end
# rubocop:enable Metrics/AbcSize