Initialize Synology bind directories explicitly and preserve failed bootstrap journals

This commit is contained in:
Pigway
2026-10-08 00:54:15 +08:00
parent e927df9c62
commit b78b998bb8
3 changed files with 155 additions and 27 deletions
+72 -21
View File
@@ -221,7 +221,13 @@ class Deploy:
def state_io(self, payload=None):
script = (
"import json,os,sys;from pathlib import Path;p=Path('/state/deploy.json');"
"os.umask(0o077);os.chmod('/state',0o700);v=json.load(sys.stdin);q=p.with_suffix('.tmp');q.write_text(json.dumps(v));q.replace(p)"
"os.umask(0o077);os.chmod('/state',0o700);v=json.load(sys.stdin);"
"old=json.loads(p.read_text()) if p.exists() else {};"
"owner=old.get('owner','');"
"assert not owner or (len(owner)==32 and all(c in '0123456789abcdef' for c in owner));"
"archive=p.with_name('deploy.'+owner+'.json');"
"archive.write_text(p.read_text()) if owner and owner!=v.get('owner') and not archive.exists() else None;"
"q=p.with_suffix('.tmp');q.write_text(json.dumps(v));q.replace(p)"
if payload is not None
else "from pathlib import Path;print(Path('/state/deploy.json').read_text())"
)
@@ -449,11 +455,73 @@ class Deploy:
raise ValueError("Bootstrap refused: target directory nonempty or unsafe")
volume = self.run("volume", "ls", "--format", "{{.Name}}")
if self.env["PROD_STATE_VOLUME"] in volume.stdout.decode().splitlines():
raise ValueError(
"Bootstrap refused: existing deployment journal requires review"
volume_info = json.loads(
self.run("volume", "inspect", self.env["PROD_STATE_VOLUME"]).stdout
)[0]
if (volume_info.get("Labels") or {}).get(
"frameflow.deploy.project"
) != self.env["PROD_PROJECT"]:
raise ValueError("Bootstrap refused: foreign journal volume")
record = json.loads(self.state_io())
candidate = record.get("candidate", {})
service = candidate.get("services", {}).get("pigway-frame-flow", {})
sources = {
m.get("target"): m.get("source") for m in service.get("volumes", [])
}
if (
record.get("kind") != "bootstrap"
or record.get("status") != "failed"
or candidate.get("name") != self.env["PROD_PROJECT"]
or service.get("container_name") != self.env["PROD_CONTAINER"]
or sources != {"/data": paths[0], "/config": paths[1]}
or service.get("ports")
!= [f"{self.env['PROD_WEB_BIND']}:{self.env['PROD_WEB_PORT']}:18787"]
):
raise ValueError(
"Bootstrap refused: journal is not a failed, matching first install"
)
print(
"Manual retry of failed bootstrap with empty targets; prior journal will be archived",
flush=True,
)
return old
def initialize_directories(self):
mounts, names = [], []
for key, target in (
("PROD_DATA_DIR", "/parent-data"),
("PROD_CONFIG_DIR", "/parent-config"),
):
path = Path(self.env[key])
if path.parent == Path("/"):
raise ValueError("Refusing writable NAS root mount")
mounts += ["--mount", f"type=bind,source={path.parent},target={target}"]
names.append(str(Path(target) / path.name))
# Synology rejects missing bind sources even with -v. Mount only existing
# explicit parents, then mkdir/chown/chmod only the two validated leaves.
code = (
"import json,os,sys;from pathlib import Path;"
"paths=[Path(v) for v in json.load(sys.stdin)];"
"assert all(not p.is_symlink() and (not p.exists() or (p.is_dir() and not any(p.iterdir()))) for p in paths);"
"[(p.mkdir(exist_ok=True),os.chown(p,10333,10333),os.chmod(p,0o750)) for p in paths]"
)
self.run(
"run",
"--rm",
"-i",
"--user",
"0:0",
"--network",
"none",
*mounts,
"--entrypoint",
"python",
self.old_image,
"-c",
code,
data=json.dumps(names).encode(),
)
def bootstrap_config(self):
config = yaml.safe_load((ROOT / "docker-compose.yml").read_text())
config["name"] = self.env["PROD_PROJECT"]
@@ -549,24 +617,7 @@ class Deploy:
self.state_io(self.state)
if residue:
self.run("rm", residue["Id"])
# -v creates only the two explicit daemon-host paths. No parent is mounted writable.
self.run(
"run",
"--rm",
"--user",
"0:0",
"--network",
"none",
"-v",
self.env["PROD_DATA_DIR"] + ":/data",
"-v",
self.env["PROD_CONFIG_DIR"] + ":/config",
"--entrypoint",
"python",
self.old_image,
"-c",
"import os;from pathlib import Path;paths=[Path('/data'),Path('/config')];assert all(not any(p.iterdir()) for p in paths);[(os.chown(p,10333,10333),os.chmod(p,0o750)) for p in paths]",
)
self.initialize_directories()
self.activate(candidate)
self.verify(candidate)
self.preflight()