Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/backup/targets/database.rb')
-rw-r--r--lib/backup/targets/database.rb283
1 files changed, 283 insertions, 0 deletions
diff --git a/lib/backup/targets/database.rb b/lib/backup/targets/database.rb
new file mode 100644
index 00000000000..dbcb48b9e7d
--- /dev/null
+++ b/lib/backup/targets/database.rb
@@ -0,0 +1,283 @@
+# frozen_string_literal: true
+
+require 'yaml'
+
+module Backup
+ module Targets
+ class Database < Target
+ 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, options:, force:)
+ super(progress, options: options)
+ @force = force
+ end
+
+ override :dump
+
+ def dump(destination_dir, _)
+ FileUtils.mkdir_p(destination_dir)
+
+ each_database(destination_dir) do |backup_connection|
+ pg_env = backup_connection.database_configuration.pg_env_variables
+ active_record_config = backup_connection.database_configuration.activerecord_variables
+ pg_database_name = active_record_config[:database]
+
+ dump_file_name = file_name(destination_dir, backup_connection.connection_name)
+ FileUtils.rm_f(dump_file_name)
+
+ progress.print "Dumping PostgreSQL database #{pg_database_name} ... "
+
+ schemas = []
+
+ if Gitlab.config.backup.pg_schema
+ schemas << Gitlab.config.backup.pg_schema
+ schemas.push(*Gitlab::Database::EXTRA_SCHEMAS.map(&:to_s))
+ end
+
+ pg_dump = ::Gitlab::Backup::Cli::Utils::PgDump.new(
+ database_name: pg_database_name,
+ snapshot_id: backup_connection.snapshot_id,
+ schemas: schemas,
+ env: pg_env)
+
+ success = Backup::Dump::Postgres.new.dump(dump_file_name, pg_dump)
+
+ backup_connection.release_snapshot! if backup_connection.snapshot_id
+
+ raise DatabaseBackupError.new(active_record_config, dump_file_name) unless success
+
+ report_success(success)
+ progress.flush
+ end
+ ensure
+ ::Gitlab::Database::EachDatabase.each_connection(
+ only: base_models_for_backup.keys, include_shared: false
+ ) do |connection, _|
+ Gitlab::Database::TransactionTimeoutSettings.new(connection).restore_timeouts
+ end
+ end
+
+ override :restore
+
+ def restore(destination_dir, _)
+ base_models_for_backup.each do |database_name, _|
+ backup_connection = Backup::DatabaseConnection.new(database_name)
+
+ config = backup_connection.database_configuration.activerecord_variables
+
+ 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)
+
+ pg_env = backup_connection.database_configuration.pg_env_variables
+ success = with_transient_pg_env(pg_env) do
+ decompress_rd, decompress_wr = IO.pipe
+ decompress_pid = spawn(decompress_cmd, out: decompress_wr, in: db_file_name)
+ decompress_wr.close
+
+ status, @errors =
+ case config[:adapter]
+ when "postgresql" then
+ progress.print "Restoring PostgreSQL database #{database} ... "
+ execute_and_track_errors(pg_restore_cmd(database), decompress_rd)
+ end
+ decompress_rd.close
+
+ Process.waitpid(decompress_pid)
+ $?.success? && status.success?
+ end
+
+ 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:
+ #{help_page_url('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 = database_name.to_sym != :main ? "#{database_name}_" : ''
+
+ 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 report_success(success)
+ if success
+ progress.puts '[DONE]'.color(:green)
+ else
+ progress.puts '[FAILED]'.color(:red)
+ end
+ end
+
+ private
+
+ def drop_tables(database_name)
+ puts_time 'Cleaning the database ... '.color(:blue)
+
+ if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database_name}"
+ Rake::Task["gitlab:db:drop_tables:#{database_name}"].invoke
+ else
+ # In single database (single or two connections)
+ Rake::Task["gitlab:db:drop_tables"].invoke
+ end
+
+ puts_time 'done'.color(:green)
+ end
+
+ # @deprecated This will be removed when restore operation is refactored to use extended_env directly
+ def with_transient_pg_env(extended_env)
+ ENV.merge!(extended_env)
+ result = yield
+ ENV.reject! { |k, _| extended_env.key?(k) }
+
+ result
+ end
+
+ def pg_restore_cmd(database)
+ ['psql', database]
+ end
+
+ def each_database(destination_dir, &block)
+ databases = []
+
+ # each connection will loop through all database connections defined in `database.yml`
+ # and reject the ones that are shared, so we don't get duplicates
+ #
+ # we consider a connection to be shared when it has `database_tasks: false`
+ ::Gitlab::Database::EachDatabase.each_connection(
+ only: base_models_for_backup.keys, include_shared: false
+ ) do |_, database_connection_name|
+ backup_connection = Backup::DatabaseConnection.new(database_connection_name)
+ databases << backup_connection
+
+ next unless multiple_databases?
+
+ begin
+ # Trigger a transaction snapshot export that will be used by pg_dump later on
+ backup_connection.export_snapshot!
+ rescue ActiveRecord::ConnectionNotEstablished
+ raise Backup::DatabaseBackupError.new(
+ backup_connection.database_configuration.activerecord_variables,
+ file_name(destination_dir, database_connection_name)
+ )
+ end
+ end
+
+ databases.each(&block)
+ end
+
+ def multiple_databases?
+ Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES
+ end
+
+ def help_page_url(path, anchor = nil)
+ ::Gitlab::Routing.url_helpers.help_page_url(path, anchor: anchor)
+ end
+ end
+ end
+end