Define scoped Forgejo native backup on the Scaleway primary

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
codex 2026-09-06 00:48:25 +02:00
parent 63b5d6d426
commit 978383fadb
6 changed files with 188 additions and 2 deletions

View file

@ -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 || \

View file

@ -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}

View file

@ -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

View file

@ -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
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

View file

@ -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())

View file

@ -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.