spec/chef/gitlab-ctl-commands-ee/lib/patroni_spec.rb (187 lines of code) (raw):
require 'spec_helper'
require 'omnibus-ctl'
require 'optparse'
require_relative('../../../../files/gitlab-ctl-commands/lib/gitlab_ctl')
require_relative('../../../../files/gitlab-ctl-commands-ee/lib/patroni')
RSpec.describe 'Patroni' do
core_commands = %w(bootstrap check-leader check-replica check-standby-leader reinitialize-replica)
additional_commands = %w(members pause resume failover switchover restart reload)
all_commands = core_commands + additional_commands
command_lines = {
'bootstrap' => %w(--srcdir=SRCDIR --scope=SCOPE --datadir=DATADIR),
'pause' => %w(-w),
'resume' => %w(--wait),
'failover' => %w(--master MASTER --candidate CANDIDATE),
'switchover' => %w(--master MASTER --candidate CANDIDATE --scheduled SCHEDULED),
'reinitialize-replica' => %w(--wait --member MEMBER),
'restart' => [],
'reload' => []
}
command_options = {
'bootstrap' => { srcdir: 'SRCDIR', scope: 'SCOPE', datadir: 'DATADIR' },
'pause' => { wait: true },
'resume' => { wait: true },
'failover' => { master: 'MASTER', candidate: 'CANDIDATE' },
'switchover' => { master: 'MASTER', candidate: 'CANDIDATE', scheduled: 'SCHEDULED' },
'reinitialize-replica' => { wait: true, member: 'MEMBER' },
'restart' => {},
'reload' => {}
}
patronictl_command = {
'members' => 'list',
'pause' => 'pause -w',
'resume' => 'resume -w',
'failover' => 'failover --force --master MASTER --candidate CANDIDATE',
'switchover' => 'switchover --force --master MASTER --candidate CANDIDATE --scheduled SCHEDULED',
'restart' => 'restart --force fake-scope fake-node',
'reload' => 'reload --force fake-scope fake-node'
}
describe '.parse_options' do
before do
allow(Patroni::Utils).to receive(:warn_and_exit).and_call_original
allow(Kernel).to receive(:exit) { |code| raise "Kernel.exit(#{code})" }
allow(Kernel).to receive(:warn)
end
it 'should throw error when global options are invalid' do
expect { Patroni.parse_options(%w(patroni --foo)) }.to raise_error(OptionParser::ParseError)
end
it 'should throw error when sub-command is not specified' do
expect { Patroni.parse_options(%w(patroni -v)) }.to raise_error(OptionParser::ParseError)
end
it 'should throw error when sub-command is not defined' do
expect { Patroni.parse_options(%w(patroni -v foo)) }.to raise_error(OptionParser::ParseError)
end
it 'should recognize global options' do
expect(Patroni.parse_options(%w(patroni -v -q members))).to include(quiet: true, verbose: true)
end
context 'when sub-command is passed' do
all_commands.each do |cmd|
it "should parse #{cmd} options" do
cmd_line = command_lines[cmd] || []
cmd_opts = command_options[cmd] || {}
cmd_opts[:command] = cmd
expect(Patroni.parse_options(%W(patroni #{cmd}) + cmd_line)).to include(cmd_opts)
end
end
end
context 'when help option is passed' do
it 'should show help message and exit for global help option' do
expect { Patroni.parse_options(%w(patroni -h)) }.to raise_error('Kernel.exit(0)')
expect(Patroni::Utils).to have_received(:warn_and_exit).with(/Usage help/)
end
all_commands.each do |cmd|
it "should show help message and exit for #{cmd} help option" do
expect { Patroni.parse_options(%W(patroni #{cmd} -h)) }.to raise_error('Kernel.exit(0)')
expect(Patroni::Utils).to have_received(:warn_and_exit).with(instance_of(OptionParser))
end
end
end
end
describe 'additional commands' do
before do
allow(GitlabCtl::Util).to receive(:get_public_node_attributes).and_return({ 'patroni' => { 'config_dir' => '/fake' } })
allow(GitlabCtl::Util).to receive(:get_node_attributes).and_return({ 'patroni' => { 'scope' => 'fake-scope', 'name' => 'fake-node' } })
allow(GitlabCtl::Util).to receive(:run_command)
end
additional_commands.each do |cmd|
it "should run the relevant patronictl command for #{cmd}" do
Patroni.send(cmd.to_sym, command_options[cmd] || {})
expect(GitlabCtl::Util).to have_received(:run_command).with(
"/opt/gitlab/embedded/bin/patronictl -c /fake/patroni.yaml #{patronictl_command[cmd]}",
user: 'root', live: false)
end
end
end
describe 'command output on non-Patroni node' do
before do
allow(GitlabCtl::Util).to receive(:get_public_node_attributes).and_return({ 'patroni' => nil })
allow(GitlabCtl::Util).to receive(:get_node_attributes).and_return({ 'patroni' => nil })
end
additional_commands.each do |cmd|
it "should raise errors when running command #{cmd}" do
expect { Patroni.send(cmd.to_sym, command_options[cmd]) }.to(
raise_error(RuntimeError, /no Patroni configuration/)
)
end
end
end
describe '.init_db' do
before do
allow(GitlabCtl::Util).to receive(:run_command)
end
it 'should call initdb command with the specified options' do
Patroni.init_db(command_options['bootstrap'])
expect(GitlabCtl::Util).to have_received(:run_command).with('/opt/gitlab/embedded/bin/initdb -D DATADIR -E UTF8')
end
end
describe '.copy_config' do
before do
allow(FileUtils).to receive(:cp_r)
end
it 'should call initdb command with the specified options' do
Patroni.copy_config(command_options['bootstrap'])
expect(FileUtils).to have_received(:cp_r).with('SRCDIR/.', 'DATADIR')
end
end
describe '.leader?, .replica?, and .standyLeader?' do
before do
allow(GitlabCtl::Util).to receive(:get_public_node_attributes).and_return({ 'patroni' => { 'api_address' => 'http://localhost:8009' } })
allow_any_instance_of(Patroni::Client).to receive(:get).with('/leader').and_yield(Struct.new(:code).new(leader_status))
allow_any_instance_of(Patroni::Client).to receive(:get).with('/replica').and_yield(Struct.new(:code).new(replica_status))
allow_any_instance_of(Patroni::Client).to receive(:get).with('/standby-leader').and_yield(Struct.new(:code).new(standby_leader_status))
end
context 'when node is leader' do
let(:leader_status) { '200' }
let(:replica_status) { '503' }
let(:standby_leader_status) { '503' }
it 'should identify node role' do
expect(Patroni.leader?({})).to be(true)
expect(Patroni.replica?({})).to be(false)
expect(Patroni.standby_leader?({})).to be(false)
end
end
context 'when node is replica' do
let(:leader_status) { '503' }
let(:replica_status) { '200' }
let(:standby_leader_status) { '503' }
it 'should identify node role' do
expect(Patroni.leader?({})).to be(false)
expect(Patroni.replica?({})).to be(true)
expect(Patroni.standby_leader?({})).to be(false)
end
end
context 'when node is standby leader' do
let(:leader_status) { '503' }
let(:replica_status) { '503' }
let(:standby_leader_status) { '200' }
it 'should identify node role' do
expect(Patroni.leader?({})).to be(false)
expect(Patroni.replica?({})).to be(false)
expect(Patroni.standby_leader?({})).to be(true)
end
end
end
describe '.reinitialize_replica' do
before do
allow(GitlabCtl::Util).to receive(:get_public_node_attributes).and_return({ 'patroni' => { 'config_dir' => '/fake' } })
allow(GitlabCtl::Util).to receive(:get_node_attributes).and_return({ 'patroni' => { 'scope' => 'fake-scope', 'name' => 'fake-node' } })
allow(GitlabCtl::Util).to receive(:run_command)
end
context 'when member option is set' do
it 'calls reinit command with the specified options to reinitialize the cluster member' do
Patroni.reinitialize_replica(command_options['reinitialize-replica'])
expect(GitlabCtl::Util).to have_received(:run_command).with(
'/opt/gitlab/embedded/bin/patronictl -c /fake/patroni.yaml reinit --force --wait fake-scope MEMBER',
user: 'root', live: true)
end
end
context 'when member option is not set' do
it 'calls reinit command with the specified options to reinitialize the current cluster member' do
Patroni.reinitialize_replica(command_options['reinitialize-replica'].slice(:wait))
expect(GitlabCtl::Util).to have_received(:run_command).with(
'/opt/gitlab/embedded/bin/patronictl -c /fake/patroni.yaml reinit --force --wait fake-scope fake-node',
user: 'root', live: true)
end
end
end
end