S19: establish engineering repository
This commit is contained in:
97
tests/acceptance.py
Normal file
97
tests/acceptance.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user