Record repaired backup lane and stage bounded consumer refresh packet
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
parent
42d71faec9
commit
afa1058062
5 changed files with 322 additions and 1 deletions
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"artifact": "root-custody-history-20261003T195656Z-a52ec01b2a4a4c28ba21a52eb29e7cb7.json.age",
|
||||
"ciphertext_sha256": "577498be3c6566e9622fe0d87ea3ad55063941acfab3aeb0accfc4a05dd21200",
|
||||
"coordinates_only_updated": true,
|
||||
"current_live_baseline_matches": true,
|
||||
"decrypted_history_matches": true,
|
||||
"digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248",
|
||||
"escrow_and_unrelated_fields_preserved": true,
|
||||
"finished_at": "2026-10-03T19:56:59.023904+00:00",
|
||||
"historical_config_present": true,
|
||||
"historical_credential_status": "historical-owner-and-revocation-unverified",
|
||||
"historical_share_matches_current_lane": false,
|
||||
"history_entries": 1,
|
||||
"independent_escrow_decryption_verified": true,
|
||||
"independent_escrow_recipient_matches_current_lane": true,
|
||||
"kv_version": 1,
|
||||
"lane_cas_noop": false,
|
||||
"lane_kv_version": 3,
|
||||
"local_historical_rclone_used": false,
|
||||
"ok": true,
|
||||
"provider": "existing-governed-nextcloud-backup",
|
||||
"provider_roundtrip_verified_before_cas": true,
|
||||
"published_lane_kv_version": 4,
|
||||
"readback_http_status": 200,
|
||||
"record_id": "nk-custody:platform-root",
|
||||
"recorded_at": "2026-10-03T19:56:48.310198+00:00",
|
||||
"replacement_share_created": true,
|
||||
"replacement_share_id": 23,
|
||||
"revision": 1,
|
||||
"runtime_consumers_refreshed": false,
|
||||
"scope": "current-owner-read-and-governed-history-provider-roundtrip",
|
||||
"stage": "complete",
|
||||
"status": "verified",
|
||||
"unknown_shares_and_old_backups_preserved": true,
|
||||
"upload_http_status": 201
|
||||
}
|
||||
30
k8s/backup-release/README.md
Normal file
30
k8s/backup-release/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Reviewed candidate backup client refresh
|
||||
|
||||
Prepared from railiance-platform source `f1a780d830a118eecdc0ada1885e7b8f95b51072`;
|
||||
immutable Forgejo ConfigMap `backup-verified-debb9e4c59c9` and its existing mount
|
||||
patch are review artifacts. This directory is excluded from ordinary GitOps
|
||||
kustomization and has not been applied. It preserves existing env and other mounts.
|
||||
|
||||
Before runtime promotion:
|
||||
|
||||
1. Repair repo-manager historical `custodian-WP-*` identifier compatibility without
|
||||
renumbering those owner records. Exact primary projection is currently unproved.
|
||||
2. Synchronize approved railiance-platform source on railiance01, so CNPG's
|
||||
existing hostPath receives the corrected public DAV header. This ConfigMap
|
||||
updates Forgejo only; it does not update CNPG.
|
||||
3. Confirm ESO's refreshed lane after KV4 publication, using safe metadata and
|
||||
privately compared credential equality if needed, never printed Secret bodies.
|
||||
4. Review the immutable bundle and scoped mount patch, then incorporate the new
|
||||
ConfigMap and volume reference into the adopted `k8s/gitops` source. Use reviewed
|
||||
ArgoCD sync rather than the old direct deployment patch helper. Keep the old
|
||||
immutable bundle until no rollback ReplicaSet references it.
|
||||
5. Verify worker recreation after source/ESO refresh, run
|
||||
`python3 scripts/inspect_backup_delivery.py --platform-root ~/railiance-platform`,
|
||||
and require both mounted source comparisons true. EnvFrom needs a new process;
|
||||
ESO Ready alone is insufficient.
|
||||
6. Obtain a separately reviewed existing bounded backup/provider receipt before
|
||||
declaring activity consumer acceptance. Do not automatically fire schedules.
|
||||
|
||||
No image update, worker restart, schedule or provider mutation has occurred.
|
||||
Current source-owned error-boundary hardening from T06 remains separate; this
|
||||
packet does not claim it was loaded by the existing image.
|
||||
208
k8s/backup-release/bundle.yaml
Normal file
208
k8s/backup-release/bundle.yaml
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: backup-verified-debb9e4c59c9
|
||||
namespace: activity-core
|
||||
labels:
|
||||
railiance.io/workplan: RPF-WP-0029
|
||||
immutable: true
|
||||
data:
|
||||
tools__cmd__forgejo-backup: "#!/usr/bin/env bash\n# tools/cmd/forgejo-backup \u2014\
|
||||
\ Forgejo Option A backup (RAIL-HO-WP-0005 T04/T09)\n#\n# Daily: forgejo dump\
|
||||
\ (blob state) + pg_dump (forgejo-db) \u2192 age \u2192 Nextcloud forgejo/\n#\
|
||||
\ Retention: 14 daily + 4 weekly uploads (Sunday); local cache keep 7 per type.\n\
|
||||
#\n# Env:\n# KUBECONFIG (default ~/.kube/config-hosteurope)\n\
|
||||
# RAILIANCE_BACKUP_NC_TOKEN Nextcloud WebDAV token\n# RAILIANCE_BACKUP_DRY_RUN=1\
|
||||
\ skip upload and remote side effects\nset -euo pipefail\n\n\nROOT=\"$(cd \"\
|
||||
$(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\n# shellcheck source=lib/railiance-print.sh\n\
|
||||
source \"${ROOT}/lib/railiance-print.sh\"\n# shellcheck source=lib/railiance-backup-common.sh\n\
|
||||
source \"${ROOT}/lib/railiance-backup-common.sh\"\n\nKUBECONFIG=\"${KUBECONFIG:-${HOME}/.kube/config-hosteurope}\"\
|
||||
\nexport KUBECONFIG\nDRY_RUN=\"${RAILIANCE_BACKUP_DRY_RUN:-0}\"\n\nFORGEJO_NAMESPACE=\"\
|
||||
${FORGEJO_NAMESPACE:-forgejo}\"\nFORGEJO_RELEASE=\"${FORGEJO_RELEASE:-forgejo}\"\
|
||||
\nFORGEJO_DB_NAMESPACE=\"${FORGEJO_DB_NAMESPACE:-databases}\"\nFORGEJO_DB_CLUSTER=\"\
|
||||
${FORGEJO_DB_CLUSTER:-forgejo-db}\"\n\nkubectl_get_pod() {\n local ns=\"$1\"\
|
||||
\ selector=\"$2\" attempt out\n for attempt in 1 2 3 4 5 6 7 8 9 10; do\n \
|
||||
\ out=\"$(kubectl get pods -n \"${ns}\" -l \"${selector}\" \\\n -o jsonpath='{.items[0].metadata.name}'\
|
||||
\ 2>/dev/null || true)\"\n if [[ -n \"${out}\" ]]; then\n echo \"${out}\"\
|
||||
\n return 0\n fi\n sleep $((attempt * 3))\n done\n return 1\n}\n\n\
|
||||
BACKUP_DIR=\"${XDG_CACHE_HOME:-$HOME/.cache}/railiance/backups/forgejo\"\nTS=\"\
|
||||
$(date -u +%Y%m%dT%H%M%SZ)\"\nDOW=\"$(date -u +%u)\" # 7 = Sunday\n\nDUMP_PLAIN=\"\
|
||||
${BACKUP_DIR}/forgejo-dump-${TS}.zip\"\nDUMP_AGE=\"${DUMP_PLAIN}.age\"\nDB_PLAIN=\"\
|
||||
${BACKUP_DIR}/forgejo-db-${TS}.sql\"\nDB_AGE=\"${DB_PLAIN}.age\"\n\ncleanup_plain()\
|
||||
\ {\n rm -f \"${DUMP_PLAIN}\" \"${DB_PLAIN}\"\n}\ntrap cleanup_plain EXIT\n\n\
|
||||
if [[ \"${DRY_RUN}\" != \"1\" ]]; then\n railiance_backup_require_openbao_lane\n\
|
||||
fi\nrailiance_backup_require_tools\n\nFORGEJO_DB_POD=\"${FORGEJO_DB_POD:-$(kubectl_get_pod\
|
||||
\ \"${FORGEJO_DB_NAMESPACE}\" \\\n \"cnpg.io/cluster=${FORGEJO_DB_CLUSTER}\"\
|
||||
\ || true)}\"\nif [[ -z \"${FORGEJO_DB_POD}\" ]]; then\n bad \"preflight\" \"\
|
||||
forgejo-db pod not found (cluster ${FORGEJO_DB_CLUSTER})\"\n exit 1\nfi\n\nif\
|
||||
\ ! kubectl wait --for=condition=Ready \"pod/${FORGEJO_DB_POD}\" \\\n -n \"${FORGEJO_DB_NAMESPACE}\"\
|
||||
\ --timeout=180s >/dev/null 2>&1; then\n bad \"preflight\" \"forgejo-db pod not\
|
||||
\ Ready (check: make forgejo-db-status)\"\n exit 1\nfi\n\nPROD_POD=\"$(kubectl_get_pod\
|
||||
\ \"${FORGEJO_NAMESPACE}\" \\\n \"app.kubernetes.io/instance=${FORGEJO_RELEASE}\"\
|
||||
\ || true)\"\nif [[ -z \"${PROD_POD}\" ]]; then\n bad \"preflight\" \"Forgejo\
|
||||
\ app pod not found in ${FORGEJO_NAMESPACE}\"\n exit 1\nfi\n\nmkdir -p \"${BACKUP_DIR}\"\
|
||||
\nprint_hdr \"Forgejo backup \u2014 ${TS}\"\nok \"kubeconfig\" \"${KUBECONFIG}\"\
|
||||
\nok \"forgejo pod\" \"${FORGEJO_NAMESPACE}/${PROD_POD}\"\nok \"forgejo-db\" \"\
|
||||
${FORGEJO_DB_NAMESPACE}/${FORGEJO_DB_POD}\"\n\n# 1. Application dump (repos, packages,\
|
||||
\ attachments, LFS, avatars)\nok \"forgejo dump\" \"running in pod\u2026\"\npython3\
|
||||
\ \"${ROOT}/scripts/capture_forgejo_archive.py\" \\\n --namespace \"${FORGEJO_NAMESPACE}\"\
|
||||
\ --pod \"${PROD_POD}\" --output \"${DUMP_PLAIN}\"\nok \"forgejo dump\" \"$(du\
|
||||
\ -h \"${DUMP_PLAIN}\" | awk '{print $1}') $(basename \"${DUMP_PLAIN}\")\"\n\n\
|
||||
# 2. PostgreSQL logical dump (plain SQL to stdout; CNPG root FS is read-only)\n\
|
||||
ok \"pg_dump\" \"database forgejo\u2026\"\nkubectl exec -n \"${FORGEJO_DB_NAMESPACE}\"\
|
||||
\ \"${FORGEJO_DB_POD}\" -c postgres -- \\\n pg_dump -U postgres -d forgejo --no-owner\
|
||||
\ --no-acl > \"${DB_PLAIN}\"\nok \"pg_dump\" \"$(du -h \"${DB_PLAIN}\" | awk '{print\
|
||||
\ $1}') $(basename \"${DB_PLAIN}\")\"\n\n# 3. Encrypt\nrailiance_backup_encrypt\
|
||||
\ \"${DUMP_PLAIN}\" \"${DUMP_AGE}\"\nrailiance_backup_encrypt \"${DB_PLAIN}\"\
|
||||
\ \"${DB_AGE}\"\nok \"age\" \"encrypted dump + db\"\n\nif [[ \"${DRY_RUN}\" ==\
|
||||
\ \"1\" ]]; then\n warn \"dry-run\" \"skipping Nextcloud upload\"\nelse\n railiance_backup_nc_upload\
|
||||
\ \"${DUMP_AGE}\" \"forgejo-dump-${TS}.zip.age\"\n ok \"upload\" \"forgejo-dump-${TS}.zip.age\"\
|
||||
\n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-${TS}.sql.age\"\n ok\
|
||||
\ \"upload\" \"forgejo-db-${TS}.sql.age\"\n\n if [[ \"${DOW}\" == \"7\" ]]; then\n\
|
||||
\ railiance_backup_nc_upload \"${DUMP_AGE}\" \"forgejo-dump-weekly-${TS}.zip.age\"\
|
||||
\n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-weekly-${TS}.sql.age\"\
|
||||
\n ok \"weekly\" \"Sunday copies uploaded\"\n fi\n\n railiance_backup_record_success\
|
||||
\ \"${BACKUP_DIR}\" \"${TS}\"\nfi\n\n# 4. Local cache prune (keep 7 per type)\n\
|
||||
railiance_backup_prune_local \"${BACKUP_DIR}\" 'forgejo-dump-*.zip.age' 7\nrailiance_backup_prune_local\
|
||||
\ \"${BACKUP_DIR}\" 'forgejo-db-*.sql.age' 7\nrailiance_backup_prune_local \"\
|
||||
${BACKUP_DIR}\" 'forgejo-dump-*.zip' 3\nrailiance_backup_prune_local \"${BACKUP_DIR}\"\
|
||||
\ 'forgejo-db-*.sql' 3\nok \"cache\" \"local copies pruned\"\n\necho\nok \"done\"\
|
||||
\ \"Forgejo backup complete \u2014 ${TS}\"\nif [[ \"${DRY_RUN}\" != \"1\" ]];\
|
||||
\ then\n echo \" Dump: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-dump-${TS}.zip.age\"\
|
||||
\n echo \" DB: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-db-${TS}.sql.age\"\
|
||||
\n echo\n echo \" Decrypt with ~/.config/age/railiance-backup.key\"\n echo\
|
||||
\ \" Restore drill: railiance-infra tools/forgejo-restore-drill.sh\"\nfi"
|
||||
scripts__capture_forgejo_archive.py: "#!/usr/bin/env python3\n\"\"\"Capture only\
|
||||
\ a completed, byte-identical and CRC-verified Forgejo ZIP.\"\"\"\nimport argparse\n\
|
||||
import hashlib\nfrom pathlib import Path\nimport secrets\nimport subprocess\n\
|
||||
import time\nimport zipfile\n\n\nESSENTIALS_FLAGS = ['--skip-package-data', '--skip-log',\
|
||||
\ '--skip-index', '--skip-repo-archives']\n\n\ndef validate_archive(path, profile=\"\
|
||||
full\"):\n with zipfile.ZipFile(path) as archive:\n if sum(i.file_size\
|
||||
\ for i in archive.infolist()) > 20*1024**3:\n raise ValueError('archive\
|
||||
\ exceeds recovery size bound')\n names = set(archive.namelist())\n \
|
||||
\ if 'forgejo-db.sql' not in names or not any(n.startswith('repos/') for n\
|
||||
\ in names):\n raise ValueError('required archive content missing')\n\
|
||||
\ if profile == 'essentials' and any(n.startswith(('data/packages/', 'data/repo-archive/',\
|
||||
\ 'data/indexers/')) and not n.endswith('/') for n in names):\n raise\
|
||||
\ ValueError('excluded data present in essentials archive')\n if archive.testzip()\
|
||||
\ is not None:\n raise ValueError('archive checksum failure')\n\n\n\
|
||||
def capture(namespace, pod, destination, profile=\"full\"):\n if profile not\
|
||||
\ in (\"full\", \"essentials\"): raise ValueError(\"unknown archive profile\"\
|
||||
)\n k = ['kubectl', 'exec', '--request-timeout=120s', '-n', namespace, pod,\
|
||||
\ '-c', 'gitea', '--']\n def call(args, timeout=150):\n r = subprocess.run(k\
|
||||
\ + args, capture_output=True, timeout=timeout)\n if r.returncode: raise\
|
||||
\ ValueError('capture command failed')\n return r.stdout\n remote =\
|
||||
\ '/tmp/wp0029-backup-' + secrets.token_hex(12)\n completed = False\n try:\n\
|
||||
\ # Completion belongs to this exact process, not a global pgrep or file\
|
||||
\ existence.\n flags = ' '.join(ESSENTIALS_FLAGS) if profile == 'essentials'\
|
||||
\ else ''\n command = f'umask 077; forgejo dump {flags} -f {remote}.zip\
|
||||
\ >{remote}.log 2>&1; result=$?; printf \"%s\" \"$result\" >{remote}.exit'\n \
|
||||
\ call(['sh','-c', 'nohup sh -c \"$1\" >/dev/null 2>&1 </dev/null &', 'sh',\
|
||||
\ command])\n for _ in range(180):\n r = subprocess.run(k +\
|
||||
\ ['cat',remote+'.exit'], capture_output=True, timeout=30)\n if r.returncode\
|
||||
\ == 0:\n completed = True\n if r.stdout.strip()\
|
||||
\ != b'0': raise ValueError('dump process failed')\n break\n \
|
||||
\ time.sleep(10)\n else: raise ValueError('dump completion timeout')\n\
|
||||
\ size = int(call(['stat','-c%s',remote+'.zip']).strip())\n expected\
|
||||
\ = call(['sha256sum',remote+'.zip']).split()[0].decode()\n if size <=\
|
||||
\ 0: raise ValueError('empty archive')\n chunk = 4*1024*1024\n with\
|
||||
\ destination.open('xb') as out:\n destination.chmod(0o600)\n \
|
||||
\ for index in range((size+chunk-1)//chunk):\n piece = call(['dd','if='+remote+'.zip','bs='+str(chunk),'skip='+str(index),'count=1'])\n\
|
||||
\ if len(piece) != min(chunk,size-index*chunk): raise ValueError('short\
|
||||
\ archive chunk')\n out.write(piece)\n with destination.open('rb')\
|
||||
\ as source:\n actual = hashlib.file_digest(source,'sha256').hexdigest()\n\
|
||||
\ if actual != expected: raise ValueError('archive transfer mismatch')\n\
|
||||
\ validate_archive(destination, profile)\n if profile == \"essentials\"\
|
||||
:\n from forgejo_essentials_profile import seal\n seal(destination)\n\
|
||||
\ validate_archive(destination, profile)\n finally:\n # Do\
|
||||
\ not remove a file while a timed-out producer may still be writing it.\n \
|
||||
\ if completed:\n call(['rm','-f',remote+'.zip',remote+'.log',remote+'.exit'])\n\
|
||||
\n\ndef main():\n p=argparse.ArgumentParser(description=__doc__)\n p.add_argument('--namespace',required=True);\
|
||||
\ p.add_argument('--pod',required=True)\n p.add_argument('--output',required=True,type=Path)\n\
|
||||
\ p.add_argument(\"--profile\",choices=[\"full\",\"essentials\"],default=\"\
|
||||
full\")\n a=p.parse_args()\n try: capture(a.namespace,a.pod,a.output,a.profile)\n\
|
||||
\ except Exception as error:\n print('ERROR: Forgejo archive capture\
|
||||
\ or integrity validation failed ('+type(error).__name__+')')\n return\
|
||||
\ 1\n print('Forgejo archive capture and integrity verified')\n return 0\n\
|
||||
\nif __name__=='__main__': raise SystemExit(main())\n"
|
||||
lib__railiance-backup-common.sh: "#!/usr/bin/env bash\n# lib/railiance-backup-common.sh\
|
||||
\ \u2014 age encrypt + Nextcloud upload helpers (Option A)\nset -euo pipefail\n\
|
||||
\n: \"${RAILIANCE_BACKUP_AGE_PUBLIC_KEY:=age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz}\"\
|
||||
\n: \"${RAILIANCE_BACKUP_BAO_PATH:=platform/workloads/railiance/backup/offsite-lane}\"\
|
||||
\n: \"${RAILIANCE_BACKUP_NC_PREFIX:=forgejo}\"\n\nrailiance_backup_load_openbao_lane()\
|
||||
\ {\n if [[ -n \"${RAILIANCE_BACKUP_NC_TOKEN:-}\" && -n \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:-}\"\
|
||||
\ ]]; then\n return 0\n fi\n command -v bao >/dev/null 2>&1 || return 0\n\
|
||||
\ bao kv metadata get \"${RAILIANCE_BACKUP_BAO_PATH}\" >/dev/null 2>&1 || return\
|
||||
\ 0\n : \"${RAILIANCE_BACKUP_NC_TOKEN:=$(bao kv get -field=NC_WEBDAV_TOKEN \"\
|
||||
${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=$(bao\
|
||||
\ kv get -field=NC_WEBDAV_URL \"${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\
|
||||
\n}\n\nrailiance_backup_require_openbao_lane() {\n railiance_backup_load_openbao_lane\n\
|
||||
\ if [[ -z \"${RAILIANCE_BACKUP_NC_TOKEN:-}\" ]]; then\n echo \"ERROR: governed\
|
||||
\ backup credential unavailable (CCR-2026-0004)\" >&2\n return 1\n fi\n #\
|
||||
\ Build a file-drop URL only after the required token is present.\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/dav/filesdrop/${RAILIANCE_BACKUP_NC_TOKEN}}\"\
|
||||
\n}\n\nrailiance_backup_require_tools() {\n # Prefer vendored static binaries\
|
||||
\ (activity-core hostPath / offline runners).\n local root vendor\n root=\"\
|
||||
$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n vendor=\"${root}/tools/vendor/bin\"\
|
||||
\n if [[ -d \"$vendor\" ]]; then\n export PATH=\"${vendor}:${PATH}\"\n fi\n\
|
||||
\ if [[ -n \"${RAILIANCE_BACKUP_AGE_BIN:-}\" && -x \"${RAILIANCE_BACKUP_AGE_BIN}\"\
|
||||
\ ]]; then\n export PATH=\"$(dirname \"${RAILIANCE_BACKUP_AGE_BIN}\"):${PATH}\"\
|
||||
\n fi\n local t\n for t in age kubectl; do\n command -v \"$t\" >/dev/null\
|
||||
\ 2>&1 || { echo \"ERROR: missing required tool: $t (run tools/cmd/install-cnpg-backup-vendor-tools)\"\
|
||||
\ >&2; exit 1; }\n done\n # curl optional \u2014 upload falls back to python3\
|
||||
\ urllib (activity-core worker has no curl)\n if ! command -v curl >/dev/null\
|
||||
\ 2>&1 && ! command -v python3 >/dev/null 2>&1; then\n echo \"ERROR: need curl\
|
||||
\ or python3 for Nextcloud upload\" >&2\n exit 1\n fi\n}\n\nrailiance_backup_nc_upload()\
|
||||
\ {\n local file=\"$1\" remote_name=\"$2\"\n local dest=\"${RAILIANCE_BACKUP_NC_WEBDAV_URL%/}/${RAILIANCE_BACKUP_NC_PREFIX}/${remote_name}\"\
|
||||
\n if [[ \"$dest\" != https://* || \"$dest\" == *$'\\n'* || \"$dest\" == *$'\\\
|
||||
r'* ||\n \"${RAILIANCE_BACKUP_NC_TOKEN}\" == *$'\\n'* || \"${RAILIANCE_BACKUP_NC_TOKEN}\"\
|
||||
\ == *$'\\r'* ||\n \"$file\" == *$'\\n'* || \"$file\" == *$'\\r'* ]]; then\n\
|
||||
\ echo \"ERROR: invalid backup upload input\" >&2\n return 1\n fi\n if\
|
||||
\ command -v curl >/dev/null 2>&1; then\n # Credentials and credential-bearing\
|
||||
\ URLs travel over stdin, never argv.\n local config_url config_token config_file\
|
||||
\ status\n config_url=\"${dest//\\\\/\\\\\\\\}\"; config_url=\"${config_url//\\\
|
||||
\"/\\\\\\\"}\"\n config_token=\"${RAILIANCE_BACKUP_NC_TOKEN//\\\\/\\\\\\\\\
|
||||
}\"; config_token=\"${config_token//\\\"/\\\\\\\"}\"\n config_file=\"${file//\\\
|
||||
\\/\\\\\\\\}\"; config_file=\"${config_file//\\\"/\\\\\\\"}\"\n status=\"$(printf\
|
||||
\ 'url = \"%s\"\\nuser = \"%s:\"\\nupload-file = \"%s\"\\n' \\\n \"$config_url\"\
|
||||
\ \"$config_token\" \"$config_file\" | \\\n curl --disable --silent --fail\
|
||||
\ --proto '=https' --max-time 600 \\\n --header 'X-Requested-With: XMLHttpRequest'\
|
||||
\ \\\n --output /dev/null --write-out '%{http_code}' --config - 2>/dev/null)\"\
|
||||
\ \\\n || { echo \"ERROR: Nextcloud upload failed\" >&2; return 1; }\n \
|
||||
\ case \"$status\" in\n 200|201|204) ;;\n *) echo \"ERROR: Nextcloud\
|
||||
\ upload failed\" >&2; return 1 ;;\n esac\n return 0\n fi\n # Python fallback\
|
||||
\ (worker image)\n RAILIANCE_BACKUP_NC_TOKEN=\"${RAILIANCE_BACKUP_NC_TOKEN}\"\
|
||||
\ \\\n RAILIANCE_BACKUP_UPLOAD_URL=\"${dest}\" \\\n RAILIANCE_BACKUP_UPLOAD_FILE=\"\
|
||||
${file}\" \\\n python3 - <<'PY' || { echo \"ERROR: Nextcloud upload failed\"\
|
||||
\ >&2; return 1; }\nimport os, urllib.request, urllib.error, base64, sys\nurl\
|
||||
\ = os.environ[\"RAILIANCE_BACKUP_UPLOAD_URL\"]\npath = os.environ[\"RAILIANCE_BACKUP_UPLOAD_FILE\"\
|
||||
]\ntoken = os.environ[\"RAILIANCE_BACKUP_NC_TOKEN\"]\nclass NoRedirect(urllib.request.HTTPRedirectHandler):\n\
|
||||
\ def redirect_request(self, req, fp, code, msg, headers, newurl):\n \
|
||||
\ raise urllib.error.HTTPError(req.full_url, code, 'redirect refused', headers,\
|
||||
\ fp)\ntry:\n with open(path, 'rb') as source:\n chunks = iter(lambda:\
|
||||
\ source.read(1024 * 1024), b'')\n req = urllib.request.Request(url, data=chunks,\
|
||||
\ method=\"PUT\")\n req.add_header(\"Authorization\", \"Basic \" + base64.b64encode(f\"\
|
||||
{token}:\".encode()).decode())\n req.add_header(\"X-Requested-With\", \"\
|
||||
XMLHttpRequest\")\n req.add_header(\"Content-Type\", \"application/octet-stream\"\
|
||||
)\n req.add_header(\"Content-Length\", str(os.fstat(source.fileno()).st_size))\n\
|
||||
\ with urllib.request.build_opener(NoRedirect()).open(req, timeout=600)\
|
||||
\ as resp:\n if resp.status not in (200, 201, 204):\n \
|
||||
\ sys.exit(1)\nexcept Exception:\n # URLs may themselves contain credentials:\
|
||||
\ never emit exception text.\n sys.exit(1)\nPY\n}\n\nrailiance_backup_encrypt()\
|
||||
\ {\n local src=\"$1\" dest=\"$2\"\n age -r \"${RAILIANCE_BACKUP_AGE_PUBLIC_KEY}\"\
|
||||
\ -o \"$dest\" \"$src\"\n}\n\nrailiance_backup_prune_local() {\n local dir=\"\
|
||||
$1\" pattern=\"$2\" keep=\"${3:-7}\"\n find \"$dir\" -maxdepth 1 -name \"$pattern\"\
|
||||
\ -type f 2>/dev/null \\\n | sort -r | tail -n +\"$((keep + 1))\" | xargs -r\
|
||||
\ rm -f\n}\n\nrailiance_backup_record_success() {\n local stamp_dir=\"$1\" ts=\"\
|
||||
$2\"\n mkdir -p \"$stamp_dir\"\n echo \"$ts\" > \"${stamp_dir}/.last-success\"\
|
||||
\n echo \"$ts\" >> \"${stamp_dir}/success-log\"\n}\n"
|
||||
lib__railiance-print.sh: "#!/usr/bin/env bash\n# lib/railiance-print.sh \u2014 shared\
|
||||
\ pretty printers for platform operator tools\nset -euo pipefail\n\nprint_hdr()\
|
||||
\ { printf \"\\n%s\\n\" \"$1\"; printf \"%0.s-\" $(seq 1 \"${#1}\"); echo; }\n\
|
||||
ok() { printf \" \u2705 %-12s %s\\n\" \"$1\" \"$2\"; }\nwarn() { printf \"\
|
||||
\ \u26A0\uFE0F %-12s %s\\n\" \"$1\" \"$2\"; }\nbad() { printf \" \u274C %-12s\
|
||||
\ %s\\n\" \"$1\" \"$2\"; }"
|
||||
entrypoint: '#!/bin/sh
|
||||
|
||||
exec /opt/railiance-backup-verified/tools/cmd/forgejo-backup "$@"
|
||||
|
||||
'
|
||||
29
k8s/backup-release/worker-patch.yaml
Normal file
29
k8s/backup-release/worker-patch.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- name: backup-verified
|
||||
configMap:
|
||||
name: backup-verified-debb9e4c59c9
|
||||
defaultMode: 365
|
||||
items:
|
||||
- key: tools__cmd__forgejo-backup
|
||||
path: tools/cmd/forgejo-backup
|
||||
- key: scripts__capture_forgejo_archive.py
|
||||
path: scripts/capture_forgejo_archive.py
|
||||
- key: lib__railiance-backup-common.sh
|
||||
path: lib/railiance-backup-common.sh
|
||||
- key: lib__railiance-print.sh
|
||||
path: lib/railiance-print.sh
|
||||
- key: entrypoint
|
||||
path: entrypoint
|
||||
containers:
|
||||
- name: worker
|
||||
volumeMounts:
|
||||
- name: backup-verified
|
||||
mountPath: /opt/railiance-backup-verified
|
||||
readOnly: true
|
||||
- name: backup-verified
|
||||
mountPath: /opt/railiance-platform/tools/cmd/forgejo-backup
|
||||
subPath: entrypoint
|
||||
readOnly: true
|
||||
|
|
@ -267,7 +267,7 @@ id: ACTIVITY-WP-0042-T08
|
|||
status: wait
|
||||
depends_on: [RPF-WP-0049-T05]
|
||||
priority: high
|
||||
blocking_reason: "Platform must supply an authenticated replacement-lane/CAS and encrypted history upload/readback receipt; then a reviewed scoped worker bundle/hostPath and env refresh is required."
|
||||
blocking_reason: "Provider prerequisite verified at lane KV4; scoped consumer bundle/hostPath and env refresh remain unperformed. Exact primary projection also blocked by existing historical identifier compatibility in repo-manager."
|
||||
state_hub_task_id: "020d19be-0576-5052-ad35-14b1d5a48fe1"
|
||||
```
|
||||
|
||||
|
|
@ -316,3 +316,21 @@ CAS from lane version 3 to 4; accept only the actual verified publication
|
|||
receipt, not that forecast. `runtime_consumers_refreshed: false` correctly
|
||||
leaves this T08 pending. Shared header source is `50da37c`; no broader backup
|
||||
readiness follows from initial root-history provider proof.
|
||||
|
||||
|
||||
2026-10-03T19:56:59Z supersedes the missing replacement-lane prerequisite:
|
||||
producer repair succeeded, lane CAS3->4 changed only token/URL, existing
|
||||
escrow/unrelated fields preserved, encrypted upload201/readback200 and
|
||||
independent decryption verified. Non-secret receipt retained in
|
||||
`docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json`.
|
||||
`runtime_consumers_refreshed: false`; T08 remains waiting solely on reviewed
|
||||
consumer delivery and its acceptance. Root-history provider proof is complete.
|
||||
|
||||
Concrete review packet `k8s/backup-release/README.md` and its source-owned
|
||||
immutable Forgejo bundle/worker patch are staged outside ordinary GitOps
|
||||
rendering. No runtime apply/restart occurred. CNPG requires separate primary
|
||||
checkout delivery. `rmgr sync --path . --push` currently fails preflight on
|
||||
existing historic `custodian-WP-*` identifiers, before projection; source Git
|
||||
and local Hub sync succeed. Do not renumber grandfathered IDs or claim exact
|
||||
primary projection. This existing repo-manager compatibility issue must be
|
||||
resolved by its owner before that projection-based rollout path.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue