spec/chef/cookbooks/pgbouncer/recipes/pgbouncer_spec.rb (390 lines of code) (raw):

# # Copyright:: Copyright (c) 2017 GitLab Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef_helper' RSpec.describe 'pgbouncer' do let(:chef_run) { ChefSpec::SoloRunner.new(step_into: %w(runit_service)).converge('gitlab-ee::default') } let(:pgbouncer_ini) { '/var/opt/gitlab/pgbouncer/pgbouncer.ini' } let(:databases_json) { '/var/opt/gitlab/pgbouncer/databases.json' } let(:default_vars) do { 'SSL_CERT_DIR' => '/opt/gitlab/embedded/ssl/certs/' } end before do allow(Gitlab).to receive(:[]).and_call_original end describe 'when enabled' do before do stub_gitlab_rb( pgbouncer: { enable: true, databases: { gitlabhq_production: { host: '1.2.3.4' } } }, postgresql: { pgbouncer_user: 'fakeuser', pgbouncer_user_password: 'fakeuserpassword' } ) end it 'includes the pgbouncer recipe' do expect(chef_run).to include_recipe('pgbouncer::enable') end it 'includes the postgresql user recipe' do expect(chef_run).to include_recipe('postgresql::user') end it_behaves_like 'enabled runit service', 'pgbouncer', 'root', 'root' it 'creates necessary env variable files' do expect(chef_run).to create_env_dir('/opt/gitlab/etc/pgbouncer/env').with_variables(default_vars) end it 'creates the appropriate directories' do expect(chef_run).to create_directory('/var/opt/gitlab/pgbouncer') end it 'installs pgbouncer.ini with default values' do # Default values are pulled from: # https://github.com/pgbouncer/pgbouncer/blob/6ef66f0139b9c8a5c0747f2a6157d008b87bf0c5/etc/pgbouncer.ini expect(chef_run).to render_file(pgbouncer_ini).with_content { |content| expect(content).to match(/^listen_addr = 0\.0\.0\.0$/) expect(content).to match(/^listen_port = 6432$/) expect(content).to match(/^pool_mode = transaction$/) expect(content).to match(/^max_prepared_statements = 0$/) expect(content).to match(/^server_reset_query = DISCARD ALL$/) expect(content).to match(/^application_name_add_host = 1$/) expect(content).to match(/^max_client_conn = 2048$/) expect(content).to match(/^default_pool_size = 100$/) expect(content).to match(/^min_pool_size = 0$/) expect(content).to match(/^reserve_pool_size = 5$/) expect(content).to match(/^reserve_pool_timeout = 5.0$/) expect(content).to match(/^server_round_robin = 0$/) expect(content).to match(/^auth_type = md5$/) expect(content).to match(/^log_connections = 0/) expect(content).to match(/^server_idle_timeout = 30.0$/) expect(content).to match(/^dns_max_ttl = 15.0$/) expect(content).to match(/^dns_zone_check_period = 0$/) expect(content).to match(/^dns_nxdomain_ttl = 15.0$/) expect(content).to match(%r{^auth_file = /var/opt/gitlab/pgbouncer/pg_auth$}) expect(content).to match(/^admin_users = gitlab-psql, postgres, pgbouncer$/) expect(content).to match(/^stats_users = gitlab-psql, postgres, pgbouncer$/) expect(content).to match(/^ignore_startup_parameters = extra_float_digits$/) expect(content).to match(/^track_extra_parameters = IntervalStyle$/) expect(content).to match(%r{^unix_socket_dir = /var/opt/gitlab/pgbouncer$}) expect(content).to match(%r{^%include /var/opt/gitlab/pgbouncer/databases.ini}) expect(content).to match(/^unix_socket_mode = 0777$/) expect(content).to match(/^client_tls_sslmode = disable$/) expect(content).to match(/^client_tls_protocols = all$/) expect(content).to match(/^client_tls_dheparams = auto$/) expect(content).to match(/^client_tls_ecdhcurve = auto$/) expect(content).to match(/^server_tls_sslmode = disable$/) expect(content).to match(/^server_tls_protocols = all$/) expect(content).to match(/^server_tls_ciphers = fast$/) expect(content).to match(/^server_reset_query_always = 0$/) expect(content).to match(/^server_check_query = select 1$/) expect(content).to match(/^server_check_delay = 30$/) expect(content).to match(/^syslog = 0$/) expect(content).to match(/^syslog_facility = daemon$/) expect(content).to match(/^syslog_ident = pgbouncer$/) expect(content).to match(/^log_disconnections = 1$/) expect(content).to match(/^log_pooler_errors = 1$/) expect(content).to match(/^stats_period = 60$/) expect(content).to match(/^verbose = 0$/) expect(content).to match(/^server_lifetime = 3600$/) expect(content).to match(/^server_connect_timeout = 15$/) expect(content).to match(/^server_login_retry = 15$/) expect(content).to match(/^query_timeout = 0$/) expect(content).to match(/^query_wait_timeout = 120$/) expect(content).to match(/^client_idle_timeout = 0$/) expect(content).to match(/^client_login_timeout = 60$/) expect(content).to match(/^autodb_idle_timeout = 3600$/) expect(content).to match(/^suspend_timeout = 10$/) expect(content).to match(/^idle_transaction_timeout = 0$/) expect(content).to match(/^cancel_wait_timeout = 10$/) expect(content).to match(/^pkt_buf = 4096$/) expect(content).to match(/^listen_backlog = 128$/) expect(content).to match(/^sbuf_loopcnt = 5$/) expect(content).to match(/^max_packet_size = 2147483647$/) expect(content).to match(/^so_reuseport = 0$/) expect(content).to match(/^tcp_defer_accept = 0$/) expect(content).to match(/^tcp_socket_buffer = 0$/) expect(content).to match(/^tcp_keepalive = 1$/) expect(content).to match(/^tcp_keepcnt = 0$/) expect(content).to match(/^tcp_keepidle = 0$/) expect(content).to match(/^tcp_keepintvl = 0$/) expect(content).to match(/^disable_pqexec = 0$/) expect(content).not_to match(/^logfile =/) expect(content).not_to match(/^pidfile =/) expect(content).not_to match(%r{^unix_socket_group =}) expect(content).not_to match(%r{^client_tls_ca_file =}) expect(content).not_to match(%r{^client_tls_cert_file =}) expect(content).not_to match(%r{^server_tls_ca_file =}) expect(content).not_to match(%r{^server_tls_key_file =}) expect(content).not_to match(%r{^server_tls_cert_file =}) expect(content).not_to match(%r{^max_db_connections =}) expect(content).not_to match(%r{^max_user_connections =}) expect(content).not_to match(%r{^auth_dbname =}) } end context 'pgbouncer.ini template changes' do let(:template) { chef_run.template(pgbouncer_ini) } it 'stores the socket directory in a different location when set' do stub_gitlab_rb( pgbouncer: { enable: true, unix_socket_dir: '/fake/dir', unix_socket_group: 'fakegroup', client_tls_ca_file: '/fakecafile', client_tls_cert_file: '/fakecertfile', server_tls_ca_file: '/fakeservercafile', server_tls_key_file: '/fakeserverkeyfile', server_tls_cert_file: '/fakeservercertfile', max_db_connections: 99999, max_user_connections: 88888 } ) expect(chef_run).to render_file(pgbouncer_ini).with_content { |content| expect(content).to match(%r{^unix_socket_dir = /fake/dir$}) expect(content).to match(%r{^unix_socket_group = fakegroup$}) expect(content).to match(%r{^client_tls_ca_file = /fakecafile$}) expect(content).to match(%r{^client_tls_cert_file = /fakecertfile$}) expect(content).to match(%r{^server_tls_ca_file = /fakeservercafile$}) expect(content).to match(%r{^server_tls_key_file = /fakeserverkeyfile$}) expect(content).to match(%r{^server_tls_cert_file = /fakeservercertfile$}) expect(content).to match(%r{^max_db_connections = 99999$}) expect(content).to match(%r{^max_user_connections = 88888$}) } end it 'reloads pgbouncer and starts pgbouncer if it is not running' do allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).and_call_original allow_any_instance_of(OmnibusHelper).to receive(:should_notify?).with('pgbouncer').and_return(true) expect(template).to notify('execute[reload pgbouncer]').to(:run).immediately end end context 'databases.json' do it 'creates databases.json' do expect(chef_run).to create_file(databases_json) .with_content("{\"gitlabhq_production\":{\"host\":\"1.2.3.4\"}}") .with(user: 'root', group: 'gitlab-psql') end it 'notifies pgb-notify to generate databases.ini' do json_resource = chef_run.file(databases_json) expect(json_resource).to notify('execute[generate databases.ini]').to(:run).immediately end it 'does not run pgb-notify when databases.ini exists' do allow(File).to receive(:exist?).and_call_original allow(File).to receive(:exist?).with('/var/opt/gitlab/pgbouncer/databases.ini').and_return(true) expect(chef_run).not_to run_execute('generate databases.ini') end it 'stores in a different location when attribute is set' do stub_gitlab_rb( pgbouncer: { enable: true, databases_json: '/fakepath/fakedatabases.json' } ) expect(chef_run).to create_file('databases.json') .with(path: '/fakepath/fakedatabases.json') end it 'changes the user when the attribute is changed' do stub_gitlab_rb( pgbouncer: { enable: true, databases_ini_user: 'fakeuser' } ) expect(chef_run).to create_file('databases.json') .with(user: 'fakeuser', group: 'gitlab-psql') end end context 'peers' do it 'configures the peers section' do stub_gitlab_rb( pgbouncer: { enable: true, peers: { 1 => { host: 'host1', port: '9001' }, 2 => { host: 'host2', port: '9002' }, } } ) expect(chef_run).to render_file(pgbouncer_ini).with_content { |content| expect(content).to match(%r{^1 = host=host1 port=9001$}) expect(content).to match(%r{^2 = host=host2 port=9002$}) } end end end context 'authentication' do let(:pg_auth) { '/var/opt/gitlab/pgbouncer/pg_auth' } it 'sets up auth_hba when attributes are set' do stub_gitlab_rb( { pgbouncer: { enable: true, auth_hba_file: '/fake/hba_file', auth_query: 'SELECT * FROM FAKETABLE', auth_dbname: 'fakedb', } } ) expect(chef_run).to render_file(pgbouncer_ini).with_content { |content| expect(content).to match(%r{^auth_hba_file = /fake/hba_file$}) expect(content).to match(/^auth_query = SELECT \* FROM FAKETABLE$/) expect(content).to match(/^auth_dbname = fakedb$/) } end it 'does not create the user file by default' do expect(chef_run).not_to render_file(pg_auth) end it 'creates the user file when the attributes are set' do stub_gitlab_rb( { pgbouncer: { enable: true, databases: { gitlabhq_production: { password: 'fakemd5password', user: 'fakeuser', host: '127.0.0.1', port: 5432 } } } } ) expect(chef_run).to render_file(pg_auth) .with_content(%r{^"fakeuser" "md5fakemd5password"$}) end it 'creates arbitrary user' do stub_gitlab_rb( { pgbouncer: { enable: true, users: { 'fakeuser': { 'password': 'fakehash' } } } } ) expect(chef_run).to render_file(pg_auth) .with_content(%r{^"fakeuser" "md5fakehash"}) end it 'supports SCRAM secrets' do stub_gitlab_rb( pgbouncer: { enable: true, auth_type: 'scram-sha-256', users: { 'fakeuser': { 'password': 'REALLYFAKEHASH' } } } ) expect(chef_run).to render_file(pg_auth) .with_content(%r{^"fakeuser" "SCRAM-SHA-256\$REALLYFAKEHASH"}) end it 'supports a default auth type' do stub_gitlab_rb( pgbouncer: { enable: true, auth_type: 'scram-sha-256', users: { 'firstfakeuser': { 'password': 'AREALLYFAKEHASH' }, 'secondfakeuser': { 'password': 'ANOTHERREALLYFAKEHASH' } }, databases: { fakedb: { user: 'databasefakeuser', password: 'DATABASEHASH' } } } ) expect(chef_run).to render_file(pg_auth).with_content { |content| expect(content).to match(%r{^"firstfakeuser" "SCRAM-SHA-256\$AREALLYFAKEHASH"}) expect(content).to match(%r{^"secondfakeuser" "SCRAM-SHA-256\$ANOTHERREALLYFAKEHASH"}) expect(content).to match(%r{^"databasefakeuser" "SCRAM-SHA-256\$DATABASEHASH"}) } end it 'supports per user auth types' do stub_gitlab_rb( pgbouncer: { enable: true, users: { 'firstfakeuser': { 'password': 'AREALLYFAKEHASH' }, 'secondfakeuser': { 'password': 'ANOTHERREALLYFAKEHASH', 'auth_type': 'scram-sha-256' } }, databases: { fakedb: { user: 'databasefakeuser', auth_type: 'plain', password: 'DATABASEHASH' } } } ) expect(chef_run).to render_file(pg_auth).with_content { |content| expect(content).to match(%r{^"firstfakeuser" "md5AREALLYFAKEHASH"}) expect(content).to match(%r{^"secondfakeuser" "SCRAM-SHA-256\$ANOTHERREALLYFAKEHASH"}) expect(content).to match(%r{^"databasefakeuser" "DATABASEHASH"}) } end context 'when disabled by default' do it_behaves_like 'disabled runit service', 'pgbouncer' it 'includes the pgbouncer_disable recipe' do expect(chef_run).to include_recipe('pgbouncer::disable') end end end context 'log directory and runit group' do context 'default values' do before do stub_gitlab_rb(pgbouncer: { enable: true }) end it_behaves_like 'enabled logged service', 'pgbouncer', true, { log_directory_owner: 'gitlab-psql' } end context 'custom values' do before do stub_gitlab_rb( pgbouncer: { enable: true, log_group: 'fugee' } ) end it_behaves_like 'enabled logged service', 'pgbouncer', true, { log_directory_owner: 'gitlab-psql', log_group: 'fugee' } end end end RSpec.describe 'gitlab-ee::default' do let(:chef_run) { ChefSpec::SoloRunner.converge('gitlab-ee::default') } before do allow(Gitlab).to receive(:[]).and_call_original stub_gitlab_rb( { pgbouncer: { db_user_password: 'fakeuserpassword' }, postgresql: { pgbouncer_user: 'fakeuser', pgbouncer_user_password: 'fakeuserpassword' } } ) end it 'should create the pgbouncer user on the database' do expect(chef_run).to include_recipe('pgbouncer::user') expect(chef_run).to create_pgbouncer_user('rails:main').with( password: 'fakeuserpassword' ) end end