mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 18:22:00 +00:00
389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""给 deploy/deploy.sh 的测试用的假 docker(tests/test_deploy_script.py):不碰本机的 docker,状态放一个 JSON 文件里。
|
||
|
||
只认 deploy.sh 用到的那些子命令和 inspect 格式,认不出的直接报错退出(测试就会失败),免得假的悄悄放过脚本的新用法。
|
||
每次调用记一行 JSON(argv)到 FAKE_DOCKER_LOG,测试按它核对先后顺序和参数。用完就删的容器(run --rm)里要做的事照做:
|
||
python -m app.ops 真跑(/data 换成挂进来的目录),chown 不做,mkdir、pg_dump 在目录里建文件,copy-to-postgres 按
|
||
FAKE_COPY_FAIL 成功或失败。
|
||
|
||
几个开关(环境变量):
|
||
FAKE_COPY_FAIL=dry|real copy-to-postgres 的核对(--dry-run)或正式导入失败
|
||
FAKE_PGDUMP_FAIL=1 pg_dump 失败
|
||
FAKE_PSQL_FAIL=1 docker exec … psql 失败
|
||
FAKE_UNHEALTHY_ENV=<变量名> 带这个环境变量(-e)起的容器健康检查不过
|
||
FAKE_UNHEALTHY_IMAGE=<镜像> 用这个镜像起的容器健康检查不过
|
||
FAKE_RESTARTING_IMAGE=<镜像> 用这个镜像起的容器一直在重启(restarting)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import platform
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
BACKEND = Path(__file__).resolve().parents[1]
|
||
# 测试里 import 这个模块只为 host_arch,不设这两个变量
|
||
STATE = Path(os.environ.get("FAKE_DOCKER_STATE", "fake-docker-state.json"))
|
||
LOG = Path(os.environ.get("FAKE_DOCKER_LOG", "fake-docker-calls.jsonl"))
|
||
|
||
VALUE_OPTS = {
|
||
"--name", "--label", "--restart", "--network", "-e", "--env", "-v", "--volume", "-p", "--publish", "--env-file",
|
||
"--entrypoint", "--user", "-u", "--health-cmd", "--health-interval", "--health-timeout", "--health-retries",
|
||
"--health-start-period", "--stop-timeout", "--log-opt", "--security-opt", "--device", "--group-add", "--tmpfs",
|
||
}
|
||
FLAG_OPTS = {"-d", "--detach", "--rm", "-i", "-t", "-it"}
|
||
|
||
|
||
def host_arch() -> str:
|
||
machine = platform.machine().lower()
|
||
return {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(machine, machine)
|
||
|
||
|
||
def load() -> dict:
|
||
state = json.loads(STATE.read_text()) if STATE.exists() else {}
|
||
state.setdefault("images", {})
|
||
state.setdefault("containers", {})
|
||
# 网络、数据卷:名字 → 标签。测试里直接写列表(只有名字、没标签)的也认
|
||
for kind in ("networks", "volumes"):
|
||
value = state.get(kind) or {}
|
||
state[kind] = {name: {} for name in value} if isinstance(value, list) else value
|
||
return state
|
||
|
||
|
||
def save(state: dict) -> None:
|
||
STATE.write_text(json.dumps(state, ensure_ascii=False, indent=1))
|
||
|
||
|
||
def fail(message: str, code: int = 1) -> None:
|
||
print(message, file=sys.stderr)
|
||
sys.exit(code)
|
||
|
||
|
||
def unknown(argv: list[str]) -> None:
|
||
fail(f"fake docker: 认不出 {argv!r}", 99)
|
||
|
||
|
||
def image_info(state: dict, ref: str) -> dict | None:
|
||
return state["images"].get(ref)
|
||
|
||
|
||
def container_health(c: dict) -> str:
|
||
if c["state"] != "running":
|
||
return c.get("health", "")
|
||
bad_env = os.environ.get("FAKE_UNHEALTHY_ENV")
|
||
if bad_env and bad_env in c["env"]:
|
||
return "unhealthy"
|
||
if os.environ.get("FAKE_UNHEALTHY_IMAGE") and c["image"] == os.environ["FAKE_UNHEALTHY_IMAGE"]:
|
||
return "unhealthy"
|
||
return "healthy"
|
||
|
||
|
||
def fmt_container(c: dict, fmt: str) -> str:
|
||
if fmt == "{{.State.Status}}":
|
||
return c["state"]
|
||
if fmt == "{{.State.Running}}":
|
||
return "true" if c["state"] == "running" else "false"
|
||
if fmt == "{{.Config.Image}}":
|
||
return c["image"]
|
||
if ".Mounts" in fmt and (m := re.search(r'eq \.Destination "([^"]+)"', fmt)):
|
||
# .Source(绑定挂载的宿主机路径)和 .Name(数据卷名)这里存的是同一个值
|
||
return c["mounts"].get(m.group(1), "")
|
||
if ".State.Health" in fmt:
|
||
return container_health(c)
|
||
if ".Config.Labels" in fmt and (m := re.search(r'index \.Config\.Labels "([^"]+)"', fmt)):
|
||
return c["labels"].get(m.group(1), "")
|
||
if ".HostConfig.PortBindings" in fmt:
|
||
return "".join(p.rsplit(":", 2)[-2] for p in c["ports"])
|
||
raise KeyError(fmt)
|
||
|
||
|
||
def fmt_image(img: dict, fmt: str) -> str:
|
||
if fmt == "{{.Architecture}}":
|
||
return img["arch"]
|
||
if fmt == "{{.Os}}/{{.Architecture}}":
|
||
return f"linux/{img['arch']}"
|
||
if fmt == "{{.Config.User}}":
|
||
return img.get("user", "")
|
||
if '"agentlab.microvm"' in fmt:
|
||
return img.get("microvm", "0")
|
||
raise KeyError(fmt)
|
||
|
||
|
||
def parse_run(args: list[str]) -> tuple[dict, list[str]]:
|
||
opts: dict = {"env": {}, "labels": {}, "mounts": {}, "ports": [], "flags": set(), "volumes": []}
|
||
i = 0
|
||
while i < len(args):
|
||
a = args[i]
|
||
if a in FLAG_OPTS:
|
||
opts["flags"].add(a)
|
||
i += 1
|
||
continue
|
||
if a.startswith("-") and "=" in a and a.split("=", 1)[0] in VALUE_OPTS:
|
||
a, value = a.split("=", 1)
|
||
i += 1
|
||
elif a in VALUE_OPTS:
|
||
value = args[i + 1]
|
||
i += 2
|
||
elif a.startswith("-"):
|
||
unknown(["run", *args])
|
||
else:
|
||
break
|
||
if a in ("-e", "--env"):
|
||
key, _, val = value.partition("=")
|
||
opts["env"][key] = val
|
||
elif a == "--label":
|
||
key, _, val = value.partition("=")
|
||
opts["labels"][key] = val
|
||
elif a in ("-v", "--volume"):
|
||
src, dst = value.split(":")[:2]
|
||
opts["mounts"][dst] = src
|
||
if not src.startswith("/"):
|
||
opts["volumes"].append(src)
|
||
elif a in ("-p", "--publish"):
|
||
opts["ports"].append(value)
|
||
else:
|
||
opts[a] = value
|
||
return opts, args[i:]
|
||
|
||
|
||
def to_host(arg: str, data: str | None) -> str:
|
||
if data and (arg == "/data" or arg.startswith("/data/")):
|
||
return data + arg[len("/data"):]
|
||
return arg
|
||
|
||
|
||
def run_oneoff(state: dict, opts: dict, image: str, command: list[str]) -> int:
|
||
data = opts["mounts"].get("/data")
|
||
entry = opts.get("--entrypoint")
|
||
if entry == "python" and command[:2] == ["-m", "app.ops"]:
|
||
sys.path.insert(0, str(BACKEND))
|
||
from app import ops
|
||
|
||
rest = [to_host(a, data) for a in command[2:]]
|
||
if rest and rest[0] in ("postgres-secrets", "state", "backup-sqlite", "backup-dir") and "--data" not in rest:
|
||
rest[1:1] = ["--data", data or "/data"]
|
||
return ops.main(rest)
|
||
if entry == "python" and command[:3] == ["-m", "app.db", "copy-to-postgres"]:
|
||
which = "dry" if "--dry-run" in command else "real"
|
||
if os.environ.get("FAKE_COPY_FAIL") == which:
|
||
print("没有导入:fake 的失败", file=sys.stderr)
|
||
return 1
|
||
print("核对通过")
|
||
return 0
|
||
if entry == "chown":
|
||
return 0
|
||
if entry == "mkdir":
|
||
for a in command:
|
||
if not a.startswith("-"):
|
||
Path(to_host(a, data)).mkdir(parents=True, exist_ok=True)
|
||
return 0
|
||
if entry == "sh" and command[:1] == ["-c"] and 'pg_dump "$@"' in command[1]:
|
||
# sh -c '<脚本>' pg_dump <参数…>:照脚本里的 umask 建文件
|
||
umask = 0o077 if "umask 077" in command[1] else 0o022
|
||
if os.environ.get("FAKE_PGDUMP_FAIL"):
|
||
print("pg_dump: fake 的失败", file=sys.stderr)
|
||
return 1
|
||
args = command[3:]
|
||
out = Path(to_host(args[args.index("-f") + 1], data))
|
||
fd = os.open(out, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o666 & ~umask)
|
||
with os.fdopen(fd, "wb") as fh:
|
||
fh.write(b"PGDMP fake")
|
||
return 0
|
||
unknown(["run", "--rm", image, *command])
|
||
return 99
|
||
|
||
|
||
def main(argv: list[str]) -> int:
|
||
with LOG.open("a") as fh:
|
||
fh.write(json.dumps(argv, ensure_ascii=False) + "\n")
|
||
state = load()
|
||
if not argv:
|
||
unknown(argv)
|
||
cmd, args = argv[0], argv[1:]
|
||
|
||
if cmd == "version":
|
||
print("27.0.0-fake")
|
||
return 0
|
||
|
||
if cmd == "image" and args[:1] == ["inspect"]:
|
||
fmt = None
|
||
rest = args[1:]
|
||
if rest[:1] == ["-f"]:
|
||
fmt, rest = rest[1], rest[2:]
|
||
img = image_info(state, rest[0])
|
||
if img is None:
|
||
fail(f"Error: No such image: {rest[0]}")
|
||
if fmt:
|
||
try:
|
||
print(fmt_image(img, fmt))
|
||
except KeyError:
|
||
unknown(argv)
|
||
else:
|
||
print("[{}]")
|
||
return 0
|
||
|
||
if cmd == "load":
|
||
tar = Path(args[args.index("-i") + 1])
|
||
for line in tar.read_text().splitlines():
|
||
ref, _, user = line.partition(" ")
|
||
if ref:
|
||
state["images"][ref] = {"arch": host_arch(), "user": user, "microvm": "0"}
|
||
print(f"Loaded image: {ref}")
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd == "rmi":
|
||
for ref in args:
|
||
state["images"].pop(ref, None)
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd == "container" and args[:1] == ["inspect"]:
|
||
return 0 if args[1] in state["containers"] else 1
|
||
|
||
if cmd == "inspect":
|
||
if args[:1] != ["-f"]:
|
||
unknown(argv)
|
||
fmt, name = args[1], args[2]
|
||
c = state["containers"].get(name)
|
||
if c is None:
|
||
fail(f"Error: No such object: {name}")
|
||
try:
|
||
print(fmt_container(c, fmt))
|
||
except KeyError:
|
||
unknown(argv)
|
||
return 0
|
||
|
||
if cmd == "run":
|
||
opts, rest = parse_run(args)
|
||
image, command = rest[0], rest[1:]
|
||
if image_info(state, image) is None:
|
||
fail(f"Unable to find image '{image}' locally", 125)
|
||
network = opts.get("--network")
|
||
if network and network not in ("none", "host", "bridge") and network not in state["networks"]:
|
||
fail(f"docker: Error response from daemon: network {network} not found.", 125)
|
||
if "--rm" in opts["flags"]:
|
||
return run_oneoff(state, opts, image, command)
|
||
name = opts.get("--name")
|
||
if not name or "-d" not in opts["flags"]:
|
||
unknown(argv)
|
||
if name in state["containers"]:
|
||
fail(f'docker: Error response from daemon: Conflict. The container name "/{name}" is already in use.', 125)
|
||
for vol in opts["volumes"]:
|
||
state["volumes"].setdefault(vol, {})
|
||
flapping = os.environ.get("FAKE_RESTARTING_IMAGE") == image
|
||
state["containers"][name] = {
|
||
"image": image, "state": "restarting" if flapping else "running", "env": opts["env"],
|
||
"labels": opts["labels"], "mounts": opts["mounts"], "ports": opts["ports"], "network": network or "bridge",
|
||
"restart": opts.get("--restart", "no"),
|
||
}
|
||
save(state)
|
||
print("f" * 64)
|
||
return 0
|
||
|
||
if cmd == "update":
|
||
name = args[-1]
|
||
c = state["containers"].get(name)
|
||
if c is None:
|
||
fail(f"Error response from daemon: No such container: {name}")
|
||
for i, a in enumerate(args[:-1]):
|
||
if a.startswith("--restart="):
|
||
c["restart"] = a.split("=", 1)[1]
|
||
elif a == "--restart":
|
||
c["restart"] = args[i + 1]
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd in ("start", "stop", "restart"):
|
||
names = [a for a in args if not a.startswith("-") and not a.isdigit()]
|
||
for name in names:
|
||
c = state["containers"].get(name)
|
||
if c is None:
|
||
fail(f"Error response from daemon: No such container: {name}")
|
||
c["state"] = "exited" if cmd == "stop" else "running"
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd == "rm":
|
||
force = "-f" in args
|
||
for name in (a for a in args if not a.startswith("-")):
|
||
c = state["containers"].get(name)
|
||
if c is None:
|
||
fail(f"Error response from daemon: No such container: {name}")
|
||
if c["state"] == "running" and not force:
|
||
fail(f"Error response from daemon: cannot remove container {name}: container is running")
|
||
del state["containers"][name]
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd == "rename":
|
||
old, new = args
|
||
if old not in state["containers"] or new in state["containers"]:
|
||
fail(f"Error response from daemon: cannot rename {old} to {new}")
|
||
state["containers"][new] = state["containers"].pop(old)
|
||
save(state)
|
||
return 0
|
||
|
||
if cmd == "exec":
|
||
name, command = args[0], args[1:]
|
||
c = state["containers"].get(name)
|
||
if c is None or c["state"] != "running":
|
||
fail(f"Error response from daemon: container {name} is not running")
|
||
if command[:1] == ["psql"]:
|
||
return 1 if os.environ.get("FAKE_PSQL_FAIL") else 0
|
||
if command[:1] == ["python"]:
|
||
print("子进程(fake)")
|
||
return 0
|
||
if command[:1] == ["pg_isready"]:
|
||
return 0
|
||
unknown(argv)
|
||
|
||
if cmd == "logs":
|
||
print("fake 的日志")
|
||
return 0
|
||
|
||
if cmd == "port":
|
||
c = state["containers"].get(args[0])
|
||
if c is None:
|
||
return 1
|
||
for p in c["ports"]:
|
||
host_part = p.rsplit(":", 1)[0]
|
||
print(host_part)
|
||
return 0
|
||
|
||
if cmd in ("network", "volume"):
|
||
kind = cmd + "s"
|
||
sub, name = args[0], args[-1]
|
||
if sub == "inspect":
|
||
if name not in state[kind]:
|
||
return 1
|
||
if len(args) >= 3 and args[1] == "-f":
|
||
m = re.search(r'index \.Labels "([^"]+)"', args[2])
|
||
if not m:
|
||
unknown(argv)
|
||
print(state[kind][name].get(m.group(1), ""))
|
||
return 0
|
||
if sub == "create":
|
||
labels = dict(a.split("=", 1) for a in (args[i + 1] for i, x in enumerate(args) if x == "--label"))
|
||
if name in state[kind] and cmd == "network":
|
||
fail(f"network with name {name} already exists")
|
||
state[kind].setdefault(name, labels)
|
||
save(state)
|
||
return 0
|
||
if sub == "rm":
|
||
if cmd == "network" and any(c["network"] == name for c in state["containers"].values()):
|
||
fail(f"error while removing network: network {name} has active endpoints")
|
||
state[kind].pop(name, None)
|
||
save(state)
|
||
return 0
|
||
unknown(argv)
|
||
|
||
unknown(argv)
|
||
return 99
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv[1:]))
|