98 lines
5.0 KiB
Python
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()
|