mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-04-30 20:01:00 +08:00
PostgreSQL refuses to run initdb/pg_ctl as root. When TemporaryDb detects it is running as root, it now delegates all PG commands to the postgres system user via sudo. This fixes the migration-tests CI workflow where the schema validation step runs as root. Also adds a PATH-based fallback for pg_bin_path discovery to support distributions that install PG binaries outside the hardcoded paths (e.g. Fedora system packages in /usr/bin). --------- Co-authored-by: Gerhard Schlager <gerhard.schlager@discourse.org>
234 lines
5.8 KiB
Ruby
234 lines
5.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "open3"
|
|
require "securerandom"
|
|
require "tmpdir"
|
|
|
|
class TemporaryDb
|
|
PG_TEMP_PREFIX = "pg_schema_tmp"
|
|
VERSIONS = 10..30 # arbitrary upper limit to avoid updating this code for a long time
|
|
STARTUP_TIMEOUT_SECONDS = 60
|
|
DEFAULT_PG_SYSTEM_USER = "postgres"
|
|
|
|
def initialize(pg_system_user: DEFAULT_PG_SYSTEM_USER)
|
|
@pg_temp_path = File.join(Dir.tmpdir, "#{PG_TEMP_PREFIX}_#{SecureRandom.hex(6)}")
|
|
@pg_conf = "#{@pg_temp_path}/postgresql.conf"
|
|
@pg_sock_path = "#{@pg_temp_path}/sockets"
|
|
@pg_system_user = pg_system_user
|
|
end
|
|
|
|
def port_available?(port)
|
|
TCPServer.open(port).close
|
|
true
|
|
rescue Errno::EADDRINUSE
|
|
false
|
|
end
|
|
|
|
def pg_bin_path
|
|
return @pg_bin_path if @pg_bin_path
|
|
|
|
# Debian/Ubuntu: /usr/lib/postgresql/{version}/bin
|
|
VERSIONS.reverse_each do |v|
|
|
bin_path = "/usr/lib/postgresql/#{v}/bin"
|
|
return @pg_bin_path = bin_path if File.exist?("#{bin_path}/pg_ctl")
|
|
end
|
|
|
|
# RHEL/Fedora (PGDG): /usr/pgsql-{version}/bin
|
|
VERSIONS.reverse_each do |v|
|
|
bin_path = "/usr/pgsql-#{v}/bin"
|
|
return @pg_bin_path = bin_path if File.exist?("#{bin_path}/pg_ctl")
|
|
end
|
|
|
|
# macOS Postgres.app
|
|
bin_path = "/Applications/Postgres.app/Contents/Versions/latest/bin"
|
|
return @pg_bin_path = bin_path if File.exist?("#{bin_path}/pg_ctl")
|
|
|
|
# Fallback: check if pg_ctl is on PATH (e.g. Fedora system packages install to /usr/bin)
|
|
pg_ctl = `which pg_ctl 2>/dev/null`.strip
|
|
return @pg_bin_path = File.dirname(pg_ctl) if pg_ctl.present?
|
|
|
|
raise "Cannot find pg_ctl. Install the PostgreSQL server package."
|
|
end
|
|
|
|
def initdb_path
|
|
@initdb_path ||= "#{pg_bin_path}/initdb"
|
|
end
|
|
|
|
def find_free_port(range)
|
|
range.each { |port| return port if port_available?(port) }
|
|
end
|
|
|
|
def pg_port
|
|
@pg_port ||= find_free_port(11_000..11_900)
|
|
end
|
|
|
|
def pg_ctl_path
|
|
@pg_ctl_path ||= "#{pg_bin_path}/pg_ctl"
|
|
end
|
|
|
|
def start
|
|
init_data_directory
|
|
configure_ports
|
|
|
|
puts "Starting postgres on port: #{pg_port}"
|
|
@previous_discourse_pg_port = ENV["DISCOURSE_PG_PORT"]
|
|
ENV["DISCOURSE_PG_PORT"] = pg_port.to_s
|
|
|
|
start_server
|
|
@started = true
|
|
|
|
create_user
|
|
create_database
|
|
|
|
puts "PG server is ready and DB is loaded"
|
|
rescue StandardError
|
|
restore_discourse_pg_port
|
|
raise
|
|
end
|
|
|
|
def stop
|
|
@started = false
|
|
args = [pg_ctl_path, "-D", @pg_temp_path, "stop"]
|
|
args = ["sudo", "-u", @pg_system_user, *args] if running_as_root?
|
|
Open3.capture3(*args)
|
|
ensure
|
|
restore_discourse_pg_port
|
|
end
|
|
|
|
def connection_hash
|
|
{ adapter: "postgresql", database: "discourse", port: pg_port, host: "localhost" }
|
|
end
|
|
|
|
def with_env(&block)
|
|
old_host = ENV["PGHOST"]
|
|
old_user = ENV["PGUSER"]
|
|
old_port = ENV["PGPORT"]
|
|
old_dev_db = ENV["DISCOURSE_DEV_DB"]
|
|
old_rails_db = ENV["RAILS_DB"]
|
|
|
|
ENV["PGHOST"] = "localhost"
|
|
ENV["PGUSER"] = "discourse"
|
|
ENV["PGPORT"] = pg_port.to_s
|
|
ENV["DISCOURSE_DEV_DB"] = "discourse"
|
|
ENV["RAILS_DB"] = "discourse"
|
|
|
|
yield
|
|
ensure
|
|
ENV["PGHOST"] = old_host
|
|
ENV["PGUSER"] = old_user
|
|
ENV["PGPORT"] = old_port
|
|
ENV["DISCOURSE_DEV_DB"] = old_dev_db
|
|
ENV["RAILS_DB"] = old_rails_db
|
|
end
|
|
|
|
def remove
|
|
raise "Error: the database must be stopped before it can be removed" if @started
|
|
FileUtils.rm_rf @pg_temp_path
|
|
end
|
|
|
|
def migrate
|
|
raise "Error: the database must be started before it can be migrated." if !@started
|
|
ActiveRecord::Base.establish_connection(connection_hash)
|
|
|
|
puts "Running migrations on blank database!"
|
|
|
|
old_stdout = $stdout.clone
|
|
old_stderr = $stderr.clone
|
|
$stdout.reopen(File.new("/dev/null", "w"))
|
|
$stderr.reopen(File.new("/dev/null", "w"))
|
|
|
|
SeedFu.quiet = true
|
|
Rake::Task["db:migrate"].invoke
|
|
ensure
|
|
$stdout.reopen(old_stdout) if old_stdout
|
|
$stderr.reopen(old_stderr) if old_stderr
|
|
end
|
|
|
|
private
|
|
|
|
def init_data_directory
|
|
FileUtils.rm_rf @pg_temp_path
|
|
run_command!(
|
|
initdb_path,
|
|
"-D",
|
|
@pg_temp_path,
|
|
"--auth-host=trust",
|
|
"--locale=en_US.UTF-8",
|
|
"-E",
|
|
"UTF8",
|
|
error_prefix: "Failed to initialize postgres data directory",
|
|
)
|
|
end
|
|
|
|
def configure_ports
|
|
FileUtils.mkdir(@pg_sock_path)
|
|
FileUtils.chown(@pg_system_user, nil, @pg_sock_path) if running_as_root?
|
|
conf = File.read(@pg_conf)
|
|
File.write(@pg_conf, conf + "\nport = #{pg_port}\nunix_socket_directories = '#{@pg_sock_path}'")
|
|
end
|
|
|
|
def start_server
|
|
log_file = File.join(@pg_temp_path, "server.log")
|
|
run_command!(
|
|
pg_ctl_path,
|
|
"-D",
|
|
@pg_temp_path,
|
|
"-l",
|
|
log_file,
|
|
"-w",
|
|
"-t",
|
|
STARTUP_TIMEOUT_SECONDS.to_s,
|
|
"start",
|
|
error_prefix: "Failed to start postgres within #{STARTUP_TIMEOUT_SECONDS}s",
|
|
)
|
|
end
|
|
|
|
def create_user
|
|
run_command!(
|
|
"createuser",
|
|
"-h",
|
|
"localhost",
|
|
"-p",
|
|
pg_port.to_s,
|
|
"-s",
|
|
"-D",
|
|
"-w",
|
|
"discourse",
|
|
error_prefix: "Failed to create temporary postgres superuser",
|
|
)
|
|
end
|
|
|
|
def create_database
|
|
run_command!(
|
|
"createdb",
|
|
"-h",
|
|
"localhost",
|
|
"-p",
|
|
pg_port.to_s,
|
|
"discourse",
|
|
error_prefix: "Failed to create temporary postgres database",
|
|
)
|
|
end
|
|
|
|
def run_command!(*args, error_prefix:)
|
|
args = ["sudo", "-u", @pg_system_user, *args] if running_as_root?
|
|
stdout, stderr, status = Open3.capture3(*args)
|
|
return if status.success?
|
|
|
|
details = stderr.to_s.strip
|
|
details = stdout.to_s.strip if details.empty?
|
|
details = "unknown error" if details.empty?
|
|
raise "#{error_prefix}: #{details}"
|
|
rescue Errno::ENOENT => e
|
|
raise "#{error_prefix}: #{e.message}"
|
|
end
|
|
|
|
def running_as_root?
|
|
Process.uid == 0
|
|
end
|
|
|
|
def restore_discourse_pg_port
|
|
ENV["DISCOURSE_PG_PORT"] = @previous_discourse_pg_port
|
|
@previous_discourse_pg_port = nil
|
|
end
|
|
end
|