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:
parent
63b5d6d426
commit
978383fadb
6 changed files with 188 additions and 2 deletions
2
Makefile
2
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 || \
|
||||
|
|
|
|||
20
helm/forgejo-db-backup-networkpolicies.yaml
Normal file
20
helm/forgejo-db-backup-networkpolicies.yaml
Normal 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}
|
||||
14
helm/forgejo-db-backup.yaml
Normal file
14
helm/forgejo-db-backup.yaml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
60
scripts/activate_forgejo_primary_backup.py
Normal file
60
scripts/activate_forgejo_primary_backup.py
Normal 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())
|
||||
72
workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md
Normal file
72
workplans/RPF-WP-0038-forgejo-scaleway-primary-coverage.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue