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