Files
engineering/tests/acceptance.py

98 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Deploy two example revisions and roll back using the real shared remote script."""
import argparse
import os
from pathlib import Path
import shutil
import socket
import subprocess
import tempfile
import uuid
def run(args, *, cwd=None, env=None, input=None):
result = subprocess.run(args, cwd=cwd, env=env, input=input, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=600)
if result.returncode:
raise RuntimeError('Command failed: ' + args[0] + '\n' + result.stdout)
return result.stdout.strip()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('project', type=Path)
parser.add_argument('--registry', required=True)
parser.add_argument('--namespace', required=True)
parser.add_argument('--user', required=True)
parser.add_argument('--token-file', type=Path, required=True)
args = parser.parse_args()
project = args.project.resolve()
name = 'recipe-acceptance-' + uuid.uuid4().hex[:10]
env = dict(os.environ)
env['DOCKER_HOST'] = run(['docker', 'context', 'inspect', '--format', '{{.Endpoints.docker.Host}}'])
env.pop('DOCKER_CONTEXT', None)
with tempfile.TemporaryDirectory(prefix='recipe-deploy-') as temporary:
root = Path(temporary)
auth = root / 'docker-auth'
auth.mkdir()
env['DOCKER_CONFIG'] = str(auth)
run(['docker', 'login', args.registry, '--username', args.user, '--password-stdin'],
env=env, input=args.token_file.read_text().strip() + '\n')
with socket.socket() as listener:
listener.bind(('127.0.0.1', 0))
port = listener.getsockname()[1]
(root / 'production.env').write_text(f'BIND_IP=127.0.0.1\nFRONTEND_PORT={port}\nDOCKER_BIN={shutil.which("docker")}\nDOCKER_SUDO=0\nDOCKER_CONFIG={auth}\n')
os.chmod(root / 'production.env', 0o600)
source = (project / 'generated/deploy/remote.sh').read_text()
prefix = f"COMPOSE_PROJECT={name}\nDEPLOY_ROLES='backend frontend'\nDEPENDENCY_SERVICES=''\nTARGET_PLATFORM=linux/amd64\nREQUIRED_SECRETS=''\n"
script = prefix + (project / 'deploy/health.sh').read_text() + '\n' + source
image_prefix = args.registry + '/' + args.namespace + '/' + name + '-'
def remote(action, owner, *values, input=None):
return run(['sh', '-c', script, '--', action, str(root), owner, *values],
env=env, input=input)
def apply(owner, version, revision):
for role in ('backend', 'frontend'):
image = image_prefix + role + ':' + revision
context = project if role == 'backend' else project / 'frontend'
run(['docker', 'build', '--target', 'prebuilt', '--label',
'org.opencontainers.image.revision=' + revision, '-t', image, str(context)], env=env)
run(['docker', 'push', image], env=env)
remote('lock', owner)
remote('preflight', owner)
remote('stage', owner, input=(project / 'deploy/compose.yaml').read_text())
remote('stage-health', owner, input=(project / 'deploy/health.sh').read_text())
prepared = remote('prepare', owner, version, revision, image_prefix)
assert prepared.count('@sha256:') == 2
remote('pending', owner, input='{"isolated_acceptance":true}\n')
assert remote('apply', owner, owner, '60').endswith('S6_HEALTHY')
remote('commit', owner, input='{"isolated_acceptance":true}\n')
remote('finish', owner)
remote('unlock', owner)
try:
apply('first', 'v1.0.0', 'a' * 40)
apply('second', 'v1.0.1', 'b' * 40)
remote('lock', 'rollback')
remote('pending', 'rollback', input='{"rollback":true}\n')
assert remote('apply', 'rollback', 'first', '60').endswith('S6_HEALTHY')
revisions = run(['docker', 'compose', '--project-name', name, '--env-file',
str(root / 'production.env'), '--env-file', str(root / 'releases/first/release.env'),
'-f', str(root / 'releases/first/compose.yaml'), 'ps', '-q'], env=env).splitlines()
assert len(revisions) == 2
for container in revisions:
assert run(['docker', 'inspect', '--format', '{{index .Config.Labels "nl.agilecc.revision"}}', container], env=env) == 'a' * 40
remote('finish', 'rollback')
remote('unlock', 'rollback')
print('PASS: actual digest preparation, two isolated deployments, readiness and rollback to the first revision')
finally:
config = root / 'releases/first/compose.yaml'
if config.exists():
run(['docker', 'compose', '--project-name', name, '--env-file', str(root / 'production.env'),
'--env-file', str(root / 'releases/first/release.env'), '-f', str(config), 'down', '--volumes'], env=env)
if __name__ == '__main__':
main()