Files
pigway-frame-flow/scripts/ci-compose-verify.py
T

158 lines
4.6 KiB
Python
Raw Permalink Normal View History

"""Verify production image in an isolated Compose project on any Docker daemon."""
import argparse
import os
import secrets
import signal
import subprocess
import tempfile
import time
from pathlib import Path
from uuid import uuid4
import yaml
ROOT = Path(__file__).resolve().parents[1]
DOCKER = os.getenv("DOCKER_BIN", "docker")
def isolated_compose(project):
config = yaml.safe_load((ROOT / "docker-compose.yml").read_text())
config["name"] = project
service = config["services"]["pigway-frame-flow"]
for key in ("container_name", "ports", "env_file"):
service.pop(key, None)
service["image"] = f"pigway-frame-flow-ci:{project}"
service["build"]["context"] = str(ROOT)
service["environment"].update(
KOKONNA_MOCK="true",
KOKONNA_WRITE_ENABLED="false",
KOKONNA_API_KEY="",
FRAME_FLOW_TOKEN=secrets.token_urlsafe(32),
)
service["volumes"] = ["data:/data", "config:/config"]
config["volumes"] = {"data": {}, "config": {}}
return config
def compose(path, *args, **kwargs):
project = yaml.safe_load(path.read_text())["name"]
return subprocess.run(
[DOCKER, "compose", "-p", project, "-f", str(path), *args], check=True, **kwargs
)
def cleanup(path):
if not path or not path.exists():
return
config = yaml.safe_load(path.read_text())
compose(path, "down", "--volumes", "--remove-orphans", "--timeout", "10")
image = config["services"]["pigway-frame-flow"]["image"]
if (
subprocess.run(
[DOCKER, "image", "inspect", image],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
):
subprocess.run([DOCKER, "image", "rm", image], check=True)
path.unlink()
def ready(path):
project = yaml.safe_load(path.read_text())["name"]
for _ in range(30):
result = subprocess.run(
[
DOCKER,
"compose",
"-p",
project,
"-f",
str(path),
"exec",
"-T",
"pigway-frame-flow",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:18787/ready', timeout=3)",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if result.returncode == 0:
return
time.sleep(2)
raise RuntimeError("CI container /ready did not return HTTP 200")
def verify():
project = "ffci-" + uuid4().hex
with tempfile.NamedTemporaryFile(
prefix=project + "-", suffix=".yml", mode="w", delete=False
) as file:
path = Path(file.name)
config = isolated_compose(project)
yaml.safe_dump(config, file)
env = os.environ | {
"COMPOSE_FILE": str(path),
"COMPOSE_PROJECT_NAME": project,
"FRAME_FLOW_IMAGE": config["services"]["pigway-frame-flow"]["image"],
}
# The always() workflow step can clean up if this process is interrupted.
if os.getenv("GITHUB_ENV"):
with Path(os.environ["GITHUB_ENV"]).open("a") as file:
file.write(f"FRAME_FLOW_CI_COMPOSE={path}\n")
try:
subprocess.run([str(ROOT / "scripts/build-verify.sh")], env=env, check=True)
compose(path, "up", "-d")
for restart in (False, True):
if restart:
compose(path, "restart", "pigway-frame-flow")
ready(path)
compose(
path,
"exec",
"-T",
"pigway-frame-flow",
"python",
"-",
input=(ROOT / "scripts/smoke-test.py").read_bytes(),
)
container = compose(
path, "ps", "-q", "pigway-frame-flow", capture_output=True, text=True
).stdout.strip()
subprocess.run(
[
DOCKER,
"inspect",
container,
"--format",
"{{.HostConfig.RestartPolicy.Name}} {{.State.Health.Status}}",
],
check=True,
)
finally:
cleanup(path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--cleanup", action="store_true")
args = parser.parse_args()
if args.cleanup:
path = os.getenv("FRAME_FLOW_CI_COMPOSE")
cleanup(Path(path) if path else None)
else:
# Normal cancellation runs finally; SIGKILL is handled by always() below.
def cancelled(*_):
raise SystemExit(143)
signal.signal(signal.SIGTERM, cancelled)
verify()
if __name__ == "__main__":
main()