#!/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()