railiance-platform/scripts/apply-backup-publisher-bucket-policy.py
codex d217c806ca
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Add the two publisher prefixes to the backup bucket policy.
The live policy keeps the original backup application. Each new key can abort only inside its own prefix.

Assistant: grok
Assistant-Session: 01a11cdb-f9fe-7011-947c-936e72007e54
2026-10-08 23:42:25 +02:00

92 lines
3 KiB
Python

#!/usr/bin/env python3
"""Add the two publisher statements to the live backup bucket policy.
Existing statement IDs are kept. The bootstrap key writes the merged policy.
The receipt contains statement IDs and status codes only.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import boto3
from botocore.exceptions import ClientError
ROOT = Path("/home/worsch/railiance-platform")
DESIRED = ROOT / "docs/object-storage/railiance-platform-pg-backup-policy.json"
RECEIPT = Path("/home/worsch/.local/state/railiance/receipts/backup-bucket-policy-apply.json")
BUCKET = "railiance-platform-pg-backup"
ENDPOINT = "https://s3.nl-ams.scw.cloud"
def kv(path):
proc = subprocess.run(["bao", "kv", "get", "-format=json", path], capture_output=True)
if proc.returncode:
raise SystemExit(71)
return json.loads(proc.stdout)["data"]["data"]
def client(access_key, secret_key):
return boto3.client(
"s3",
region_name="nl-ams",
endpoint_url=ENDPOINT,
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
)
def sids(document):
return [item.get("Sid") for item in document.get("Statement", [])]
def merged(live, desired):
statements = list(live.get("Statement", []))
present = {item.get("Sid") for item in statements}
for item in desired.get("Statement", []):
if item.get("Sid") not in present:
statements.append(item)
present.add(item.get("Sid"))
return {
"Version": live.get("Version") or desired.get("Version"),
"Id": live.get("Id") or desired.get("Id"),
"Statement": statements,
}
def main():
backup = kv("platform/workloads/railiance/backup/object-storage")
bootstrap = kv("platform/workloads/railiance/scaleway/bootstrap")
desired = json.loads(DESIRED.read_text(encoding="utf-8"))
reader = client(backup["ACCESS_KEY"], backup["SECRET_KEY"])
live = json.loads(reader.get_bucket_policy(Bucket=BUCKET)["Policy"])
policy = merged(live, desired)
receipt = {"before": sids(live), "intended": sids(policy)}
if sids(live) == sids(policy):
receipt["put"] = "unchanged"
else:
writer = client(bootstrap["ACCESS_KEY"], bootstrap["SECRET_KEY"])
try:
writer.put_bucket_policy(Bucket=BUCKET, Policy=json.dumps(policy))
receipt["put"] = "ok"
except ClientError as exc:
receipt["put"] = exc.response.get("Error", {}).get("Code", "error")
try:
after = json.loads(reader.get_bucket_policy(Bucket=BUCKET)["Policy"])
receipt["after"] = sids(after)
except ClientError as exc:
receipt["after_error"] = exc.response.get("Error", {}).get("Code", "error")
RECEIPT.parent.mkdir(mode=0o700, exist_ok=True)
RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n")
RECEIPT.chmod(0o600)
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except Exception:
raise SystemExit(72) from None