From 978383fadb25009b01801522232f31c8ebf30808 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 6 Sep 2026 00:48:25 +0200 Subject: [PATCH] Define scoped Forgejo native backup on the Scaleway primary Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883 --- Makefile | 2 + helm/forgejo-db-backup-networkpolicies.yaml | 20 ++++++ helm/forgejo-db-backup.yaml | 14 ++++ helm/forgejo-db-cluster.yaml | 22 +++++- scripts/activate_forgejo_primary_backup.py | 60 ++++++++++++++++ ...-0038-forgejo-scaleway-primary-coverage.md | 72 +++++++++++++++++++ 6 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 helm/forgejo-db-backup-networkpolicies.yaml create mode 100644 helm/forgejo-db-backup.yaml create mode 100644 scripts/activate_forgejo_primary_backup.py create mode 100644 workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md diff --git a/Makefile b/Makefile index b524cc0..d23af46 100644 --- a/Makefile +++ b/Makefile @@ -75,6 +75,8 @@ db-logs: ## Tail gitea-db primary logs forgejo-db-deploy: ## Apply forgejo-db cnpg Cluster + NetworkPolicies on railiance01 $(KUBECTL) apply -f helm/forgejo-db-cluster.yaml $(KUBECTL) apply -f helm/forgejo-db-networkpolicies.yaml + $(KUBECTL) apply -f helm/forgejo-db-backup-networkpolicies.yaml + $(KUBECTL) apply -f helm/forgejo-db-backup.yaml forgejo-db-status: ## Show forgejo-db cnpg cluster health $(KUBECTL) cnpg status forgejo-db -n databases 2>/dev/null || \ diff --git a/helm/forgejo-db-backup-networkpolicies.yaml b/helm/forgejo-db-backup-networkpolicies.yaml new file mode 100644 index 0000000..5f1fc68 --- /dev/null +++ b/helm/forgejo-db-backup-networkpolicies.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-backup-egress-forgejo-db + namespace: databases +spec: + podSelector: + matchLabels: + cnpg.io/cluster: forgejo-db + policyTypes: [Egress] + egress: + - ports: + - {port: 443, protocol: TCP} + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - {port: 53, protocol: UDP} + - {port: 53, protocol: TCP} diff --git a/helm/forgejo-db-backup.yaml b/helm/forgejo-db-backup.yaml new file mode 100644 index 0000000..5d14839 --- /dev/null +++ b/helm/forgejo-db-backup.yaml @@ -0,0 +1,14 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: forgejo-db-daily + namespace: databases + labels: + cnpg.io/cluster: forgejo-db +spec: + schedule: "0 35 2 * * *" + backupOwnerReference: self + cluster: + name: forgejo-db + immediate: true + method: barmanObjectStore diff --git a/helm/forgejo-db-cluster.yaml b/helm/forgejo-db-cluster.yaml index db4b474..5caade1 100644 --- a/helm/forgejo-db-cluster.yaml +++ b/helm/forgejo-db-cluster.yaml @@ -4,7 +4,7 @@ # # Apply: KUBECONFIG=~/.kube/config-hosteurope make forgejo-db-deploy # Status: make forgejo-db-status -# Backup: make forgejo-backup (pg_dump + forgejo dump → Nextcloud; docs/forgejo-backup.md) +# Primary: Scaleway Barman + WAL. Secondary: make forgejo-backup (Nextcloud + age). # # Pre-condition: forgejo-db-credentials Secret in databases namespace. # See helm/forgejo-db-secret.sops.yaml.template @@ -49,4 +49,22 @@ spec: database: forgejo owner: forgejo secret: - name: forgejo-db-credentials \ No newline at end of file + name: forgejo-db-credentials + backup: + retentionPolicy: 30d + barmanObjectStore: + destinationPath: s3://railiance-platform-pg-backup/platform-pg/forgejo-db/ + endpointURL: https://s3.nl-ams.scw.cloud + s3Credentials: + accessKeyId: + name: platform-pg-backup-s3 + key: ACCESS_KEY_ID + secretAccessKey: + name: platform-pg-backup-s3 + key: ACCESS_SECRET_KEY + wal: + compression: gzip + maxParallel: 2 + data: + compression: gzip + jobs: 2 diff --git a/scripts/activate_forgejo_primary_backup.py b/scripts/activate_forgejo_primary_backup.py new file mode 100644 index 0000000..1e34857 --- /dev/null +++ b/scripts/activate_forgejo_primary_backup.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Apply only the reviewed Forgejo backup contract and verify native completion.""" +import argparse +from datetime import datetime,timezone +import json +from pathlib import Path +import subprocess +import time +import yaml +from state_hub_preflight_lane import assert_cluster +ROOT=Path(__file__).resolve().parents[1] + +def run(k,receipt): + def cmd(args,payload=None): + r=subprocess.run(k+args,input=None if payload is None else json.dumps(payload).encode(),capture_output=True,timeout=60) + if r.returncode: raise RuntimeError('backup_activation_command_failed') + return r.stdout + def get(args): return json.loads(cmd(args)) + assert_cluster(k) + desired=yaml.safe_load((ROOT/'helm/forgejo-db-cluster.yaml').read_text())['spec']['backup'] + if desired['barmanObjectStore']['destinationPath']!='s3://railiance-platform-pg-backup/platform-pg/forgejo-db/': raise RuntimeError('wrong_destination') + live=get(['-n','databases','get','cluster','forgejo-db','-o','json']) + if live['spec'].get('backup') not in (None,desired): raise RuntimeError('existing_backup_differs') + ready=any(c['type']=='Ready' and c['status']=='True' for c in live['status']['conditions']) + if not ready: raise RuntimeError('source_not_ready') + for file in ('helm/forgejo-db-backup-networkpolicies.yaml','helm/forgejo-db-backup.yaml'): + cmd(['apply','--dry-run=server','-f',str(ROOT/file)]) + patch=[{'op':'test','path':'/metadata/resourceVersion','value':live['metadata']['resourceVersion']},{'op':'add','path':'/spec/backup','value':desired}] + cmd(['-n','databases','patch','cluster','forgejo-db','--type=json','--dry-run=server','--patch-file=/dev/stdin'],patch) + cmd(['apply','-f',str(ROOT/'helm/forgejo-db-backup-networkpolicies.yaml')]) + cmd(['-n','databases','patch','cluster','forgejo-db','--type=json','--patch-file=/dev/stdin'],patch) + receipt['backup_contract_applied']=True + receipt['started_at']=datetime.now(timezone.utc).isoformat() + cmd(['apply','-f',str(ROOT/'helm/forgejo-db-backup.yaml')]) + for _ in range(120): + rows=get(['-n','databases','get','backups','-o','json'])['items'] + rows=[r for r in rows if r.get('spec',{}).get('cluster',{}).get('name')=='forgejo-db'] + good=[r for r in rows if r.get('status',{}).get('phase')=='completed'] + if good: + chosen=max(good,key=lambda r:r['metadata']['creationTimestamp']) + receipt.update(backup_name=chosen['metadata']['name'],backup_id=chosen['status'].get('backupId'),backup_phase='completed') + break + time.sleep(5) + else: raise RuntimeError('fresh_backup_not_completed') + live=get(['-n','databases','get','cluster','forgejo-db','-o','json']) + checks={c['type']:c['status']=='True' for c in live['status']['conditions']} + receipt.update(production_ready=checks.get('Ready',False),continuous_archiving=checks.get('ContinuousArchiving',False),destination=desired['barmanObjectStore']['destinationPath'],retention='30d') + if not receipt['production_ready'] or not receipt['continuous_archiving']: raise RuntimeError('source_or_archiving_not_ready') + receipt['status']='verified' + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--kubeconfig',required=True);p.add_argument('--receipt',required=True);a=p.parse_args() + receipt={'schema':'platform.forgejo-primary-backup.v1','status':'failed'} + target=Path(a.receipt) + with target.open('x') as f: + try:run(['kubectl','--kubeconfig',a.kubeconfig],receipt) + except Exception:receipt['error']='activation_or_acceptance_failed' + finally:json.dump(receipt,f,indent=2) + return int(receipt['status']!='verified') +if __name__=='__main__':raise SystemExit(main()) diff --git a/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md b/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md new file mode 100644 index 0000000..addec82 --- /dev/null +++ b/workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md @@ -0,0 +1,72 @@ +--- +id: RPF-WP-0038 +type: workplan +title: "Close Forgejo primary backup coverage on Scaleway" +domain: financials +repo: railiance-platform +status: active +owner: codex +created: "2026-09-06" +updated: "2026-09-06" +--- + +Scaleway is the user-confirmed primary; Nextcloud is independent secondary. +Follow-up to the live coverage gap in RPF-WP-0036-T03. WP-0029 retains the +secondary incident and old-share invalidation. No old backups are deleted. + +## Verify ownership and scoped primary destination + +```task +id: RPF-WP-0038-T01 +status: done +priority: high +``` + +This repository owns helm/forgejo-db-cluster.yaml and its deploy target. +Reuse CCR-2026-0012's backup runtime identity and existing databases Secret. +The reviewed bucket policy permits platform-pg/*; choose the separate native +Barman destination platform-pg/forgejo-db/. No bootstrap key, IAM mutation or +namespace expansion is required. Never write full archives into a Barman +server directory. Retention follows the existing 30-day primary contract. + +## Activate native database backup and prove fresh completion + +```task +id: RPF-WP-0038-T02 +status: progress +priority: high +``` + +Add exact Scaleway destination, HTTPS/DNS egress, continuous WAL and daily +base backup. Apply only the reviewed backup field with a resource-version +guard, preserving unrelated live settings. Require a completed fresh Backup, +continuous archiving and source readiness; record metadata only. + +## Prove isolated primary database recovery + +```task +id: RPF-WP-0038-T03 +status: todo +priority: high +``` + +Recover a new scratch cluster from the native Scaleway base backup/WAL. +Verify expected Forgejo database and nonempty repository/user/package metadata, +source readiness and scratch cleanup. This is database recovery, not combined +Forgejo blob/application recovery. + +## Establish primary full-archive delivery and application recovery + +```task +id: RPF-WP-0038-T04 +status: wait +priority: high +``` + +Choose an independent archive prefix and confirm storage retention/caller +contract, bounded worker credential delivery and multipart transfer/abort. +Then retrieve a primary full archive and execute isolated application recovery. +The existing 5.35 GB encrypted verified archive is available in private staging. +Keep Nextcloud's 10 GiB secondary budget separate. The current helper still +uploads only to Nextcloud until this task's full delivery is implemented and +verified; native database backup alone cannot close the workplan.