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
|
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-cluster.yaml
|
||||||
$(KUBECTL) apply -f helm/forgejo-db-networkpolicies.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
|
forgejo-db-status: ## Show forgejo-db cnpg cluster health
|
||||||
$(KUBECTL) cnpg status forgejo-db -n databases 2>/dev/null || \
|
$(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
|
# Apply: KUBECONFIG=~/.kube/config-hosteurope make forgejo-db-deploy
|
||||||
# Status: make forgejo-db-status
|
# 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.
|
# Pre-condition: forgejo-db-credentials Secret in databases namespace.
|
||||||
# See helm/forgejo-db-secret.sops.yaml.template
|
# See helm/forgejo-db-secret.sops.yaml.template
|
||||||
|
|
@ -50,3 +50,21 @@ spec:
|
||||||
owner: forgejo
|
owner: forgejo
|
||||||
secret:
|
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