diff --git a/CHANGELOG.md b/CHANGELOG.md index b684a59..0aece1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project follows [Semantic Versioning](https://semver.org/). - Make deployment-flow `--dry-run` stop before confirmation or mutation. - Fail standby setup before mutation when required credentials do not resolve. - Scope standby rollback to the requested host and keep existing cluster nodes intact. +- Preserve pgBackRest recovery options as one shell-safe argument during remote restore. +- Run component rollback in local Ruby context instead of an SSHKit backend scope. ## 0.9.6 - 2026-08-20 diff --git a/lib/active_postgres/components/repmgr.rb b/lib/active_postgres/components/repmgr.rb index 069b066..93d038e 100644 --- a/lib/active_postgres/components/repmgr.rb +++ b/lib/active_postgres/components/repmgr.rb @@ -1,3 +1,5 @@ +require 'shellwords' + module ActivePostgres module Components class Repmgr < Base @@ -55,7 +57,7 @@ def restart end def setup_standby_only(standby_host) - puts "Setting up standby #{standby_host} (primary will not be touched)..." + puts "Setting up standby #{standby_host} (primary stays online)..." version = config.version install_apt_packages(standby_host, "postgresql-#{version}-repmgr") @@ -909,11 +911,17 @@ def build_primary_conninfo(standby_label) end def pgbackrest_restore_arguments(primary_conninfo) + command = Shellwords.join( + [ + 'pgbackrest', '--stanza=main', '--type=standby', + "--recovery-option=primary_conninfo=#{primary_conninfo}", + 'restore' + ] + ) + [ :sudo, '-u', config.postgres_user, - 'pgbackrest', '--stanza=main', '--type=standby', - "--recovery-option=primary_conninfo=#{primary_conninfo}", - 'restore' + :bash, '-lc', command ] end diff --git a/lib/active_postgres/standby_deployment_flow.rb b/lib/active_postgres/standby_deployment_flow.rb index 8408c13..7c9e87a 100644 --- a/lib/active_postgres/standby_deployment_flow.rb +++ b/lib/active_postgres/standby_deployment_flow.rb @@ -119,14 +119,17 @@ def deploy_monitoring end def register_rollback(component_name, component) - rollback_manager.register("Uninstall #{component_name} on #{standby_host}", host: standby_host) do + target_host = standby_host + captured_logger = logger + + rollback_manager.register("Uninstall #{component_name} on #{target_host}") do if component.respond_to?(:uninstall_from) - component.uninstall_from(standby_host) + component.uninstall_from(target_host) else component.uninstall end rescue StandardError => e - logger.warn "Failed to uninstall #{component_name} on #{standby_host}: #{e.message}" + captured_logger.warn "Failed to uninstall #{component_name} on #{target_host}: #{e.message}" end end diff --git a/test/repmgr_test.rb b/test/repmgr_test.rb index 61109aa..a2a0795 100644 --- a/test/repmgr_test.rb +++ b/test/repmgr_test.rb @@ -53,12 +53,11 @@ def test_pgbackrest_restore_uses_standby_mode_and_primary_conninfo conninfo = 'host=10.0.0.10 user=replication dbname=replication application_name=aws-standby' arguments = @component.send(:pgbackrest_restore_arguments, conninfo) + assert_equal [:sudo, '-u', 'postgres', :bash, '-lc'], arguments.first(5) assert_equal [ - :sudo, '-u', 'postgres', 'pgbackrest', '--stanza=main', '--type=standby', - "--recovery-option=primary_conninfo=#{conninfo}", - 'restore' - ], arguments + "--recovery-option=primary_conninfo=#{conninfo}", 'restore' + ], Shellwords.split(arguments.fetch(5)) end def test_manual_failover_policy_disables_repmgrd diff --git a/test/standby_deployment_flow_test.rb b/test/standby_deployment_flow_test.rb index 83ea95f..7f80d2c 100644 --- a/test/standby_deployment_flow_test.rb +++ b/test/standby_deployment_flow_test.rb @@ -2,10 +2,11 @@ class StandbyDeploymentFlowTest < Minitest::Test class RollbackRecorder - attr_reader :action + attr_reader :action, :host - def register(*, &block) + def register(*, host: nil, &block) @action = block + @host = host end end @@ -36,6 +37,7 @@ def test_repmgr_rollback_targets_only_the_requested_standby rollback.action.call assert_equal ['standby.example.com'], removed + assert_nil rollback.host end private