spec/chef/gitlab-ctl-commands-ee/lib/geo/promote_spec.rb (405 lines of code) (raw):
require 'spec_helper'
require 'geo/promote'
require 'geo/promote_db'
require 'gitlab_ctl/util'
require_relative('../../../../../files/gitlab-ctl-commands-ee/lib/patroni')
RSpec.describe Geo::Promote, '#execute' do
let(:base_path) { '/opt/gitlab/embedded' }
let(:ctl) { double(base_path: base_path, data_path: '/var/opt/gitlab/postgresql/data') }
let(:config_path) { Dir.mktmpdir }
let(:gitlab_cluster_config_path) { File.join(config_path, 'gitlab-cluster.json') }
let(:options) { { force: false } }
subject(:command) { described_class.new(ctl, options) }
before do
stub_const('GitlabCluster::CONFIG_PATH', config_path)
stub_const('GitlabCluster::JSON_FILE', gitlab_cluster_config_path)
allow($stdin).to receive(:gets).and_return('y')
allow($stdout).to receive(:puts)
allow($stdout).to receive(:print)
allow(ctl).to receive(:log).with(any_args)
allow(ctl).to receive(:service_enabled?).and_return(false)
allow(command).to receive(:run_command).with(any_args)
end
around do |example|
example.run
rescue SystemExit
end
after do
FileUtils.rm_rf(config_path)
end
describe '#execute' do
context 'when not running in force mode' do
it 'asks for confirmation' do
expect { command.execute }.to output(/Are you sure you want to proceed\?/).to_stdout
end
end
context 'when running in force mode' do
let(:options) { { force: true } }
it 'does not ask for final confirmation' do
expect { command.execute }.to_not output(/Are you sure you want to proceed\?/).to_stdout
end
end
context 'when there are no services to promote' do
before do
allow(ctl).to receive(:service_enabled?).and_return(false)
end
it 'prints a message' do
expect { command.execute }.to output(
/No actions are required to promote this node./).to_stdout
end
it 'returns 0 as exit code' do
expect { command.execute }.to raise_error(SystemExit) do |error|
expect(error.status).to eq(0)
end
end
end
context 'when puma is enabled' do
before do
stub_service_enabled('puma')
end
shared_examples 'promote secondary site to primary site' do
context 'on a primary node' do
it 'does not try to promote the secondary site to primary site' do
stub_primary_node
allow(command).to receive(:run_reconfigure)
allow(command).to receive(:restart_services)
expect(command)
.not_to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:set_secondary_as_primary", live: true, env: {})
command.execute
end
end
context 'on a secondary node' do
before do
stub_secondary_node
allow(command).to receive(:run_reconfigure)
end
it 'promotes the secondary site to primary site' do
allow(command).to receive(:restart_services)
expect(command)
.to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:set_secondary_as_primary", { live: true, env: an_instance_of(Hash) })
.once
.and_return(double(error?: false))
command.execute
end
context 'with enable_silent_mode option set' do
let(:options) { { enable_silent_mode: 'true', force: false } }
it 'calls the underlying rake task with ENABLE_SILENT_MODE env var set' do
allow(command).to receive(:restart_services)
expect(command)
.to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:set_secondary_as_primary", { live: true, env: a_hash_including(ENABLE_SILENT_MODE: 'true') })
.once
.and_return(double(error?: false))
command.execute
end
end
it 'restarts the puma service' do
allow(command).to receive(:promote_to_primary)
expect(ctl)
.to receive(:run_sv_command_for_service)
.with('restart', 'puma')
.once
.and_return(double(zero?: true))
command.execute
end
end
context 'on a misconfigured node' do
it 'aborts promotion with an error message' do
stub_misconfigured_node
expect { command.execute }.to output(/Unable to detect the role of this Geo node/).to_stdout
end
end
end
context 'on a single-server secondary site' do
before do
stub_single_server_secondary_site
end
it 'toggles the Geo primary/secondary roles' do
allow(command).to receive(:run_reconfigure)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:restart_services)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq("primary" => true, "secondary" => false)
end
it 'runs reconfigure' do
allow(command).to receive(:toggle_geo_services)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:restart_services)
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).once.and_return(double(success?: true))
command.execute
end
include_examples 'promote secondary site to primary site'
end
context 'on a multiple-server secondary site' do
before do
stub_multiple_server_secondary_site
end
include_examples 'promote secondary site to primary site'
it 'disables Geo secondary settings' do
allow(command).to receive(:run_reconfigure)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:restart_services)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq("geo_secondary" => { "enable" => false })
end
it 'runs reconfigure' do
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:restart_services)
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).once.and_return(double(success?: true))
command.execute
end
end
end
context 'when gitaly is enabled' do
before do
stub_service_enabled('gitaly')
end
it 'restarts the gitaly service' do
allow(command).to receive(:toggle_geo_services)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:run_reconfigure)
expect(ctl).to receive(:run_sv_command_for_service).with('restart', 'gitaly').once.and_return(double(zero?: true))
command.execute
end
end
context 'when praefect is enabled' do
before do
stub_service_enabled('praefect')
end
it 'restarts the praefect service' do
allow(command).to receive(:toggle_geo_services)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:run_reconfigure)
expect(ctl).to receive(:run_sv_command_for_service).with('restart', 'praefect').once.and_return(double(zero?: true))
command.execute
end
end
context 'when workhorse is enabled' do
before do
stub_service_enabled('gitlab-workhorse')
end
it 'restarts the workhorse service' do
allow(command).to receive(:toggle_geo_services)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:run_reconfigure)
expect(ctl).to receive(:run_sv_command_for_service).with('restart', 'gitlab-workhorse').once.and_return(double(zero?: true))
command.execute
end
end
shared_examples 'single-server secondary site' do
context 'on a single-server secondary site' do
before do
stub_single_server_secondary_site
end
it 'toggles the Geo primary/secondary roles' do
allow(command).to receive(:run_reconfigure)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq("primary" => true, "secondary" => false)
end
it 'runs reconfigure' do
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).once.and_return(double(success?: true))
command.execute
end
end
end
shared_examples 'multiple-server secondary site' do |settings|
context 'on a multiple-server secondary site' do
before do
stub_multiple_server_secondary_site
end
it 'disables Geo secondary settings' do
allow(command).to receive(:run_reconfigure)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq(settings)
end
end
end
context 'when sidekiq is enabled' do
before do
stub_service_enabled('sidekiq')
end
include_examples 'single-server secondary site'
include_examples 'multiple-server secondary site', "geo_secondary" => { "enable" => false }
end
context 'when geo-logcursor is enabled' do
before do
allow(ctl).to receive(:service_enabled?).with('geo-logcursor').and_return(true)
end
include_examples 'single-server secondary site'
include_examples 'multiple-server secondary site', "geo_logcursor" => { "enable" => false }
end
context 'when geo-postgresql is enabled' do
before do
allow(ctl).to receive(:service_enabled?).with('geo-postgresql').and_return(true)
end
include_examples 'single-server secondary site'
include_examples 'multiple-server secondary site', "geo_postgresql" => { "enable" => false }
end
context 'when postgresql is enabled' do
before do
stub_service_enabled('postgresql')
end
context 'when PostgreSQL is in recovery mode' do
before do
stub_pg_is_in_recovery
end
it 'promotes database to begin read-write operations' do
allow(command).to receive(:run_reconfigure)
expect_any_instance_of(Geo::PromoteDb).to receive(:execute).once.and_return(true)
command.execute
end
end
context 'when PostgreSQL is not in recovery mode' do
before do
stub_pg_is_not_in_recovery
end
it 'does not promote the PostgreSQL database' do
allow(command).to receive(:run_reconfigure)
expect_any_instance_of(Geo::PromoteDb).not_to receive(:execute)
command.execute
end
end
it 'runs reconfigure' do
allow(command).to receive(:promote_database)
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).once.and_return(double(success?: true))
command.execute
end
end
context 'when patroni is enabled' do
before do
stub_service_enabled('patroni')
end
shared_examples 'promotes a Patroni leader' do
context 'when PostgreSQL is in recovery mode' do
before do
stub_pg_is_in_recovery
end
it 'promotes database to begin read-write operations' do
allow(command).to receive(:disable_patroni_standby_cluster)
allow(command).to receive(:run_reconfigure)
expect_any_instance_of(Geo::PromoteDb).to receive(:execute).once.and_return(true)
command.execute
end
end
context 'when PostgreSQL is not in recovery mode' do
before do
stub_pg_is_not_in_recovery
end
it 'does not promote the PostgreSQL database' do
allow(command).to receive(:disable_patroni_standby_cluster)
allow(command).to receive(:run_reconfigure)
expect_any_instance_of(Geo::PromoteDb).not_to receive(:execute)
command.execute
end
end
it 'disables Patroni Standby cluster settings' do
allow(command).to receive(:promote_postgresql_read_write)
allow(command).to receive(:run_reconfigure)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq("patroni" => { "standby_cluster" => { "enable" => false } })
end
it 'pauses Patroni, runs reconfigure and resume Patroni' do
allow(command).to receive(:promote_postgresql_read_write)
allow(command).to receive(:disable_patroni_standby_cluster)
expect(command).to receive(:run_command).with(patroni_pause_cmd).twice.and_return(double(success?: true))
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).twice.and_return(double(success?: true))
expect(command).to receive(:run_command).with(patroni_resume_cmd).twice.and_return(double(success?: true))
command.execute
end
end
context 'on a Patroni standby leader' do
before do
allow(Patroni::Client).to receive(:new).and_return(double(standby_leader?: true, leader?: false, replica?: false))
end
include_examples 'promotes a Patroni leader'
end
context 'on a Patroni leader' do
before do
allow(Patroni::Client).to receive(:new).and_return(double(standby_leader?: false, leader?: true, replica?: false))
end
include_examples 'promotes a Patroni leader'
end
context 'on a Patroni replica' do
before do
allow(Patroni::Client).to receive(:new).and_return(double(standby_leader?: false, leader?: false, replica?: true))
end
it 'does not promote the PostgreSQL database' do
allow(command).to receive(:run_reconfigure)
expect_any_instance_of(Geo::PromoteDb).not_to receive(:execute)
command.execute
end
it 'disables Patroni Standby cluster settings' do
allow(command).to receive(:promote_postgresql_read_write)
allow(command).to receive(:run_reconfigure)
command.execute
expect(read_file_content(gitlab_cluster_config_path)).to eq("patroni" => { "standby_cluster" => { "enable" => false } })
end
it 'runs reconfigure' do
allow(command).to receive(:promote_postgresql_read_write)
allow(command).to receive(:disable_patroni_standby_cluster)
expect(command).not_to receive(:run_command).with(patroni_pause_cmd)
expect(ctl).to receive(:run_chef).with(reconfigure_cmd).twice.and_return(double(success?: true))
expect(command).not_to receive(:run_command).with(patroni_resume_cmd)
command.execute
end
end
end
context 'when promotion succeeds' do
before do
allow(command).to receive(:ask_for_confirmation)
allow(command).to receive(:check_running_services)
allow(command).to receive(:promote_database)
allow(command).to receive(:toggle_geo_services)
allow(command).to receive(:promote_to_primary)
allow(command).to receive(:run_reconfigure)
allow(command).to receive(:restart_services)
end
it 'prints a success message' do
expect { command.execute }.to output(
/You successfully promoted the current node! It might take some time to reload the services, and for the changes to take effect./).to_stdout
end
end
let(:patroni_pause_cmd) { "#{base_path}/bin/gitlab-ctl patroni pause" }
let(:patroni_resume_cmd) { "#{base_path}/bin/gitlab-ctl patroni resume" }
let(:pg_is_in_recovery_cmd) { "#{base_path}/bin/gitlab-psql -c \"SELECT pg_is_in_recovery();\" -q -t" }
let(:reconfigure_cmd) { "#{base_path}/embedded/cookbooks/dna.json" }
def stub_service_enabled(service)
allow(ctl).to receive(:service_enabled?).with(service).and_return(true)
end
def stub_primary_node
allow(command)
.to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:site:role", live: true, env: {})
.and_return(double(error?: false, stdout: 'primary'))
end
def stub_secondary_node
allow(command)
.to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:site:role", live: true, env: {})
.and_return(double(error?: false, stdout: 'secondary'))
end
def stub_misconfigured_node
allow(command)
.to receive(:run_command)
.with("#{base_path}/bin/gitlab-rake geo:site:role", live: true, env: {})
.and_return(double(error?: true, stdout: 'misconfigured'))
end
def stub_single_server_secondary_site
allow(GitlabCtl::Util).to receive(:get_node_attributes).and_return('roles' => { 'geo-secondary' => { 'enable' => true } })
end
def stub_multiple_server_secondary_site
allow(GitlabCtl::Util).to receive(:get_node_attributes).and_return('roles' => {})
end
def stub_pg_is_in_recovery
allow(command).to receive(:run_command).with(pg_is_in_recovery_cmd, anything).and_return(double(error?: false, stdout: 't'))
end
def stub_pg_is_not_in_recovery
allow(command).to receive(:run_command).with(pg_is_in_recovery_cmd, anything).and_return(double(error?: false, stdout: 'f'))
end
def read_file_content(fullpath)
JSON.load_file(fullpath)
end
end
end