mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-24 20:18:06 +09:00
An installed PostgreSQL can switch on a transaction-mode PgBouncer sidecar through its managed override, with no host port, on the shared network. fromService gains pooledUrl; apps keep the direct connectionString unless they switch, because transaction pooling breaks prepared statements in some drivers, LISTEN and advisory locks, and the card says so.
80 lines
3.7 KiB
Python
80 lines
3.7 KiB
Python
"""PgBouncer beside an installed PostgreSQL (plan 86 §D1). Hermetic half; the
|
|
real round trip is test_real_pooler_docker.py."""
|
|
import json
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from app import db
|
|
from app.services import pooler_service
|
|
from app.services.compose_env_service import ComposeEnvService
|
|
from app.services.service_connection_service import SHARED_NETWORK, ServiceConnectionService
|
|
from app.services.template_service import TemplateService
|
|
from tests.factories import make_application
|
|
|
|
|
|
@pytest.fixture
|
|
def installed(app, tmp_path, monkeypatch):
|
|
records = {}
|
|
monkeypatch.setattr(TemplateService, 'get_config', classmethod(
|
|
lambda cls: {'repos': [], 'installed': records}))
|
|
|
|
def make(name, template_id, variables):
|
|
root = tmp_path / name
|
|
root.mkdir()
|
|
(root / 'docker-compose.yml').write_text(yaml.safe_dump({'services': {'app': {'image': 'x'}}}))
|
|
(root / '.serverkit-template.json').write_text(json.dumps(
|
|
{'template_id': template_id, 'variables': variables}))
|
|
row = make_application(db, name=name, root_path=str(root))
|
|
records[str(row.id)] = {'template_id': template_id}
|
|
return row
|
|
return make
|
|
|
|
|
|
def test_pooling_adds_a_pgbouncer_service_and_a_pooled_url(installed):
|
|
pg = installed('pg', 'postgresql', {'DB_PASSWORD': 'pa$$', 'DB_NAME': 'shop'})
|
|
assert 'pooledUrl' not in ServiceConnectionService.spec(pg)['properties']
|
|
assert 'pgbouncer' not in (ComposeEnvService.render_override(pg.root_path)['content'] or '')
|
|
|
|
pg.pooler_enabled = True
|
|
db.session.commit()
|
|
|
|
override = yaml.safe_load(ComposeEnvService.render_override(pg.root_path)['content'])
|
|
bouncer = override['services']['pgbouncer']
|
|
assert bouncer['image'] == pooler_service.POOLER_IMAGE
|
|
assert bouncer['container_name'] == 'pg-pooler'
|
|
assert bouncer['environment']['DB_HOST'] == 'app'
|
|
assert bouncer['environment']['POOL_MODE'] == 'transaction'
|
|
assert bouncer['environment']['DB_PASSWORD'] == 'pa$$$$', 'compose would interpolate $'
|
|
assert 'ports' not in bouncer
|
|
assert bouncer['networks'] == {'default': {}, SHARED_NETWORK: {}}
|
|
props = ServiceConnectionService.spec(pg)['properties']
|
|
assert props['pooledUrl'] == 'postgresql://postgres:pa%24%24@pg-pooler:5432/shop'
|
|
assert props['connectionString'].startswith('postgresql://postgres:pa%24%24@pg:5432/')
|
|
|
|
|
|
def test_only_postgres_engines_get_a_pooler(installed):
|
|
cache = installed('cache', 'redis', {'DB_PASSWORD': 'x'})
|
|
cache.pooler_enabled = True
|
|
db.session.commit()
|
|
assert not pooler_service.is_postgres_engine(cache)
|
|
assert 'pgbouncer' not in (ComposeEnvService.render_override(cache.root_path)['content'] or '')
|
|
|
|
|
|
def test_api(installed, client, auth_headers, monkeypatch):
|
|
pg = installed('pg', 'postgresql', {'DB_PASSWORD': 'x'})
|
|
cache = installed('cache', 'redis', {'DB_PASSWORD': 'x'})
|
|
monkeypatch.setattr('app.services.docker_service.DockerService.compose_up',
|
|
classmethod(lambda cls, path, **kw: {'success': True}))
|
|
got = client.get(f'/api/v1/apps/{pg.id}/pooler', headers=auth_headers).get_json()
|
|
assert got['available'] is True and got['enabled'] is False and 'LISTEN' in got['tradeoff']
|
|
assert client.get(f'/api/v1/apps/{cache.id}/pooler',
|
|
headers=auth_headers).get_json()['available'] is False
|
|
|
|
put = client.put(f'/api/v1/apps/{pg.id}/pooler', json={'enabled': True}, headers=auth_headers)
|
|
assert put.status_code == 200 and put.get_json()['applied'] is True
|
|
assert client.put(f'/api/v1/apps/{pg.id}/pooler', json={'enabled': 'yes'},
|
|
headers=auth_headers).status_code == 400
|
|
assert client.put(f'/api/v1/apps/{cache.id}/pooler', json={'enabled': True},
|
|
headers=auth_headers).status_code == 400
|