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