# frozen_string_literal: true require 'yaml' module Backup class Database < Task extend ::Gitlab::Utils::Override include Backup::Helper attr_reader :force IGNORED_ERRORS = [ # Ignore warnings /WARNING:/, # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump /does not exist$/, # User may not have permissions to drop extensions or schemas /must be owner of/ ].freeze IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze def initialize(progress, force:) super(progress) @force = force end override :dump def dump(destination_dir, backup_id) FileUtils.mkdir_p(destination_dir) snapshot_ids.each do |database_name, snapshot_id| base_model = base_models_for_backup[database_name] config = base_model.connection_db_config.configuration_hash db_file_name = file_name(destination_dir, database_name) FileUtils.rm_f(db_file_name) pg_database = config[:database] progress.print "Dumping PostgreSQL database #{pg_database} ... " pg_env(config) pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. pgsql_args << '--if-exists' pgsql_args << "--snapshot=#{snapshot_ids[database_name]}" if Gitlab.config.backup.pg_schema pgsql_args << '-n' pgsql_args << Gitlab.config.backup.pg_schema Gitlab::Database::EXTRA_SCHEMAS.each do |schema| pgsql_args << '-n' pgsql_args << schema.to_s end end success = Backup::Dump::Postgres.new.dump(pg_database, db_file_name, pgsql_args) base_model.connection.rollback_transaction raise DatabaseBackupError.new(config, db_file_name) unless success report_success(success) progress.flush end ensure base_models_for_backup.each do |_database_name, base_model| Gitlab::Database::TransactionTimeoutSettings.new(base_model.connection).restore_timeouts end end override :restore def restore(destination_dir) base_models_for_backup.each do |database_name, base_model| config = base_model.connection_db_config.configuration_hash db_file_name = file_name(destination_dir, database_name) database = config[:database] unless File.exist?(db_file_name) raise(Backup::Error, "Source database file does not exist #{db_file_name}") if main_database?(database_name) progress.puts "Source backup for the database #{database_name} doesn't exist. Skipping the task" return false end unless force progress.puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) sleep(5) end # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade drop_tables(database_name) decompress_rd, decompress_wr = IO.pipe decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name) decompress_wr.close status, @errors = case config[:adapter] when "postgresql" then progress.print "Restoring PostgreSQL database #{database} ... " pg_env(config) execute_and_track_errors(pg_restore_cmd(database), decompress_rd) end decompress_rd.close Process.waitpid(decompress_pid) success = $?.success? && status.success? if @errors.present? progress.print "------ BEGIN ERRORS -----\n".color(:yellow) progress.print @errors.join.color(:yellow) progress.print "------ END ERRORS -------\n".color(:yellow) end report_success(success) raise Backup::Error, 'Restore failed' unless success end end override :pre_restore_warning def pre_restore_warning return if force <<-MSG.strip_heredoc Be sure to stop Puma, Sidekiq, and any other process that connects to the database before proceeding. For Omnibus installs, see the following link for more information: https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations Before restoring the database, we will remove all existing tables to avoid future upgrade problems. Be aware that if you have custom tables in the GitLab database these tables and all data will be removed. MSG end override :post_restore_warning def post_restore_warning return unless @errors.present? <<-MSG.strip_heredoc There were errors in restoring the schema. This may cause issues if this results in missing indexes, constraints, or columns. Please record the errors above and contact GitLab Support if you have questions: https://about.gitlab.com/support/ MSG end protected def base_models_for_backup @base_models_for_backup ||= Gitlab::Database.database_base_models_with_gitlab_shared end def main_database?(database_name) database_name.to_sym == :main end def file_name(base_dir, database_name) prefix = if database_name.to_sym != :main "#{database_name}_" else '' end File.join(base_dir, "#{prefix}database.sql.gz") end def ignore_error?(line) IGNORED_ERRORS_REGEXP.match?(line) end def execute_and_track_errors(cmd, decompress_rd) errors = [] Open3.popen3(ENV, *cmd) do |stdin, stdout, stderr, thread| stdin.binmode out_reader = Thread.new do data = stdout.read $stdout.write(data) end err_reader = Thread.new do until (raw_line = stderr.gets).nil? warn(raw_line) errors << raw_line unless ignore_error?(raw_line) end end begin IO.copy_stream(decompress_rd, stdin) rescue Errno::EPIPE end stdin.close [thread, out_reader, err_reader].each(&:join) [thread.value, errors] end end def pg_env(config) args = { username: 'PGUSER', host: 'PGHOST', port: 'PGPORT', password: 'PGPASSWORD', # SSL sslmode: 'PGSSLMODE', sslkey: 'PGSSLKEY', sslcert: 'PGSSLCERT', sslrootcert: 'PGSSLROOTCERT', sslcrl: 'PGSSLCRL', sslcompression: 'PGSSLCOMPRESSION' } args.each do |opt, arg| # This enables the use of different PostgreSQL settings in # case PgBouncer is used. PgBouncer clears the search path, # which wreaks havoc on Rails if connections are reused. override = "GITLAB_BACKUP_#{arg}" val = ENV[override].presence || config[opt].to_s.presence ENV[arg] = val if val end end def report_success(success) if success progress.puts '[DONE]'.color(:green) else progress.puts '[FAILED]'.color(:red) end end private def drop_tables(database_name) if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database_name}" puts_time 'Cleaning the database ... '.color(:blue) Rake::Task["gitlab:db:drop_tables:#{database_name}"].invoke puts_time 'done'.color(:green) elsif Gitlab::Database.database_base_models.one? # In single database, we do not have rake tasks per database puts_time 'Cleaning the database ... '.color(:blue) Rake::Task["gitlab:db:drop_tables"].invoke puts_time 'done'.color(:green) end end def pg_restore_cmd(database) ['psql', database] end def snapshot_ids @snapshot_ids ||= base_models_for_backup.each_with_object({}) do |(database_name, base_model), snapshot_ids| Gitlab::Database::TransactionTimeoutSettings.new(base_model.connection).disable_timeouts base_model.connection.begin_transaction(isolation: :repeatable_read) snapshot_ids[database_name] = base_model.connection.execute("SELECT pg_export_snapshot() as snapshot_id;").first['snapshot_id'] end end end end