Run deployment preflight through client image
This commit is contained in:
@@ -14,7 +14,8 @@ It can therefore be used unchanged on cache and archive nodes.
|
||||
## Requirements
|
||||
|
||||
- Run it on the Docker host being checked.
|
||||
- Use Python 3.11 or newer (the script uses the standard-library `tomllib`).
|
||||
- Use Python 3.8 or newer. The script reads the mounted configuration through
|
||||
the client image, so it does not depend on the host Python TOML library.
|
||||
- Have Docker CLI access to the daemon.
|
||||
- Have the checkout containing the script available on that host, or copy only
|
||||
this script into the node's compose directory.
|
||||
|
||||
@@ -14,7 +14,6 @@ import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
@@ -121,6 +120,33 @@ def run_client_check(container: str, config: str, flag: str) -> None:
|
||||
print(result.stdout.strip())
|
||||
|
||||
|
||||
def mounted_config(container: str, config: str) -> dict[str, Any]:
|
||||
"""Read normalized non-secret config through the client image itself."""
|
||||
|
||||
program = '''import json,sys
|
||||
from pathlib import Path
|
||||
from archive_clients.config import ClientConfig
|
||||
config=ClientConfig.load(Path(sys.argv[1]))
|
||||
value={"shared_token_file":str(config.shared_token_file)}
|
||||
value["qbittorrent"]={"api_root":str(config.qbittorrent.api_root),"local_root":str(config.qbittorrent.local_root),"password_file":str(config.qbittorrent.password_file)}
|
||||
value["syncthing"]={"api_root":str(config.syncthing.api_root),"local_root":str(config.syncthing.local_root),"api_key_file":str(config.syncthing.api_key_file),"local_path_overrides":{str(api):str(local) for api,local in config.syncthing.local_path_overrides}}
|
||||
print(json.dumps(value,sort_keys=True))'''
|
||||
result = subprocess.run(
|
||||
["docker", "exec", container, "python", "-c", program, config],
|
||||
check=False, text=True, capture_output=True,
|
||||
)
|
||||
if result.returncode:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "failed"
|
||||
raise CheckFailure(f"cannot load mounted client config: {detail}")
|
||||
try:
|
||||
value = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CheckFailure("mounted client config output is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise CheckFailure("mounted client config is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def run_service_check(container: str, config: str) -> None:
|
||||
program = '''import json,sys
|
||||
from pathlib import Path
|
||||
@@ -155,13 +181,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
help="client.toml path inside the client container")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
with args.client_config.open("rb") as source:
|
||||
config = tomllib.load(source)
|
||||
qb = config["qbittorrent"]
|
||||
sync = config["syncthing"]
|
||||
client = docker_inspect(args.client_container)
|
||||
syncthing = docker_inspect(args.syncthing_container)
|
||||
qbittorrent = docker_inspect(args.qbittorrent_container)
|
||||
configured_host_path = map_path(client, args.container_config).source
|
||||
if configured_host_path.resolve(strict=False) != args.client_config.resolve(strict=False):
|
||||
raise CheckFailure(
|
||||
"--client-config does not match the file mounted into the client"
|
||||
)
|
||||
config = mounted_config(args.client_container, args.container_config)
|
||||
qb = config["qbittorrent"]
|
||||
sync = config["syncthing"]
|
||||
|
||||
client_qb = map_path(client, qb["local_root"])
|
||||
client_route = map_path(client, route_local_path(sync))
|
||||
@@ -185,7 +215,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
require_regular_secret(map_path(client, service[key]).source)
|
||||
run_client_check(args.client_container, args.container_config, "--check-config")
|
||||
run_service_check(args.client_container, args.container_config)
|
||||
except (CheckFailure, KeyError, OSError, tomllib.TOMLDecodeError) as exc:
|
||||
except (CheckFailure, KeyError, OSError) as exc:
|
||||
print(f"preflight failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("preflight passed: bind mappings, secrets, filesystem capabilities, and local APIs are healthy")
|
||||
|
||||
Reference in New Issue
Block a user