121 lines
6.3 KiB
Python
121 lines
6.3 KiB
Python
"""Own one disposable local PostgreSQL container and its synthetic volume."""
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('action', choices=['start', 'ready', 'restart', 'cleanup'])
|
|
parser.add_argument('--attempt', type=int, default=1)
|
|
args = parser.parse_args()
|
|
suffix = '' if args.attempt == 1 else '-attempt' + str(args.attempt)
|
|
WORK = Path('/tmp/custodian-forgejo-postgres-r1' + suffix)
|
|
if args.action == 'start':
|
|
WORK.mkdir(exist_ok=True)
|
|
(WORK / 'home').mkdir(exist_ok=True)
|
|
(WORK / 'docker-config').mkdir(exist_ok=True)
|
|
ENV = {'PATH': '/usr/bin:/bin', 'HOME': str(WORK / 'home'),
|
|
'DOCKER_CONFIG': str(WORK / 'docker-config'), 'DOCKER_HOST': 'unix:///var/run/docker.sock'}
|
|
|
|
def docker(*arguments):
|
|
result = subprocess.run(['docker', *arguments], env=ENV, capture_output=True,
|
|
text=True, timeout=90)
|
|
assert result.returncode == 0, 'local synthetic database command refused'
|
|
return result.stdout.strip()
|
|
|
|
def inspect(cid):
|
|
return json.loads(docker('inspect', '--format', '{{json .}}', cid))
|
|
|
|
def owned(state):
|
|
item = inspect(state['container_id'])
|
|
assert item['Config']['Labels']['custodian.source-test'] == state['trial']
|
|
assert item['HostConfig']['NetworkMode'] == 'none'
|
|
assert not item['HostConfig']['Privileged'] and not item['HostConfig']['PortBindings']
|
|
assert item['Image'] == state['image_id']
|
|
mounts = item['Mounts']
|
|
assert len(mounts) == 2
|
|
assert any(m['Type'] == 'volume' and m['Name'] == state['volume'] and m['Destination'] == '/var/lib/postgresql/data' for m in mounts)
|
|
assert any(m['Type'] == 'bind' and m['Source'] == str(WORK / 'socket') and m['Destination'] == '/socket' for m in mounts)
|
|
return item
|
|
|
|
def ready(state):
|
|
for _ in range(30):
|
|
# pg_isready can succeed against the entrypoint's temporary init server,
|
|
# before the requested database exists. Require final PID 1 plus a real
|
|
# query in that exact synthetic database instead.
|
|
process = subprocess.run(['docker', 'exec', state['container_id'], 'cat', '/proc/1/comm'],
|
|
env=ENV, capture_output=True, text=True, timeout=5)
|
|
result = subprocess.run(['docker', 'exec', state['container_id'], 'psql',
|
|
'-h', '/socket', '-U', 'custodian_test', '-d', 'custodian_source',
|
|
'-At', '-c', 'SHOW server_version_num'],
|
|
env=ENV, capture_output=True, text=True, timeout=5)
|
|
if process.returncode == result.returncode == 0 and process.stdout.strip() == 'postgres' and result.stdout.strip().isdecimal():
|
|
break
|
|
time.sleep(1)
|
|
else:
|
|
raise AssertionError('synthetic PostgreSQL did not become ready; retained')
|
|
version = result.stdout.strip()
|
|
assert version.isdecimal() and int(version) // 10000 == 16
|
|
state['server_version_num'] = int(version)
|
|
|
|
state_path = HERE / ('database-runtime' + suffix + '.json')
|
|
if args.action == 'start':
|
|
assert not state_path.exists()
|
|
trial = 'custodian-forgejo-pg-' + uuid.uuid4().hex[:12]
|
|
image = json.loads((HERE / 'public-database-image.json').read_text())['image_id']
|
|
volume = trial + '-data'
|
|
socket = WORK / 'socket'
|
|
socket.mkdir(mode=0o777)
|
|
socket.chmod(0o777) # Synthetic Unix socket only, in this new /tmp trial.
|
|
docker('volume', 'create', '--label', 'custodian.source-test=' + trial, volume)
|
|
argv = ['create', '--name', trial, '--label', 'custodian.source-test=' + trial,
|
|
'--network', 'none', '--memory', '512m', '--cpus', '1', '--pids-limit', '128',
|
|
'--shm-size', '64m', '--mount', 'type=volume,source=' + volume + ',target=/var/lib/postgresql/data',
|
|
'--mount', 'type=bind,source=' + str(socket) + ',target=/socket',
|
|
'--env', 'POSTGRES_USER=custodian_test', '--env', 'POSTGRES_DB=custodian_source',
|
|
'--env', 'PGHOST=/socket',
|
|
'--env', 'POSTGRES_HOST_AUTH_METHOD=trust', image, 'postgres',
|
|
'-c', 'listen_addresses=', '-c', 'unix_socket_directories=/var/run/postgresql,/socket',
|
|
'-c', 'unix_socket_permissions=0777', '-c', 'log_statement=none',
|
|
'-c', 'log_min_error_statement=panic']
|
|
cid = docker(*argv)
|
|
state = dict(trial=trial, container_id=cid, volume=volume, image_id=image,
|
|
creation_argv=['docker', *argv], synthetic_data_only=True,
|
|
real_credentials=False, network='none', ports_published=False,
|
|
authentication='trust on private synthetic Unix-socket fixture; not a custody/authentication proof')
|
|
state_path.write_text(json.dumps(state, indent=2) + '\n')
|
|
owned(state)
|
|
docker('start', cid)
|
|
ready(state)
|
|
state['status'] = 'ready'
|
|
state_path.write_text(json.dumps(state, indent=2) + '\n')
|
|
else:
|
|
state = json.loads(state_path.read_text())
|
|
owned(state)
|
|
if args.action == 'ready':
|
|
assert inspect(state['container_id'])['State']['Running']
|
|
ready(state)
|
|
state['status'] = 'ready'
|
|
state_path.write_text(json.dumps(state, indent=2) + '\n')
|
|
elif args.action == 'restart':
|
|
docker('restart', '--time', '10', state['container_id'])
|
|
ready(state)
|
|
(HERE / ('database-restart' + suffix + '.json')).write_text(json.dumps(dict(
|
|
status='ready_after_actual_server_restart', container_id=state['container_id'],
|
|
retained_volume=state['volume'], server_version_num=state['server_version_num'],
|
|
synthetic_data_only=True), indent=2) + '\n')
|
|
else:
|
|
docker('stop', '--time', '10', state['container_id'])
|
|
owned(state)
|
|
docker('rm', state['container_id'])
|
|
volume = json.loads(docker('volume', 'inspect', '--format', '{{json .}}', state['volume']))
|
|
assert volume['Labels']['custodian.source-test'] == state['trial']
|
|
docker('volume', 'rm', state['volume'])
|
|
(HERE / ('database-cleanup' + suffix + '.json')).write_text(json.dumps(dict(
|
|
exact_owned_container_removed=True, exact_owned_volume_removed=True,
|
|
container_id=state['container_id'], volume=state['volume'],
|
|
real_credentials=False), indent=2) + '\n')
|
|
print(json.dumps({'action': args.action, 'trial': state['trial'], 'synthetic_data_only': True}))
|