Dump PostgreSQL pods whatever image they run

The dump assumed the official image's environment and the postgres
superuser. Bitnami keeps its passwords in files, and on this cluster the
superuser password no longer matches the database, so the Gitea database
backup would have failed. The collector now resolves the credentials from
either layout, probes them before dumping so a failed login cannot
truncate the archive, and falls back to a single-database dump when only
the application user works. Verified against both databases on the server.

Also adds the Nextcloud manifest that installs it on the cluster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-03 21:32:43 +02:00
parent 8f5bc1755e
commit 1339ae864e
3 changed files with 284 additions and 4 deletions

View File

@ -10,6 +10,33 @@ use domain::DomainError;
use crate::host::CommandRunner;
/// Dumps a PostgreSQL pod without knowing which image it runs.
///
/// The official image exposes `POSTGRES_USER`/`POSTGRES_PASSWORD`, Bitnami's keeps the
/// passwords in files and names the superuser's separately. Credentials are probed before
/// dumping so a failed login cannot truncate the archive; a cluster dump is preferred and a
/// single-database dump is the fallback when only the application user works.
pub const POSTGRES_DUMP: &str = r#"
read_secret() { [ -n "$1" ] && [ -f "$1" ] && tr -d '\n' < "$1"; }
su_pw="${POSTGRES_POSTGRES_PASSWORD:-$(read_secret "$POSTGRES_POSTGRES_PASSWORD_FILE")}"
app_pw="${POSTGRES_PASSWORD:-${POSTGRESQL_PASSWORD:-$(read_secret "$POSTGRES_PASSWORD_FILE")}}"
app_user="${POSTGRES_USER:-${POSTGRESQL_USERNAME:-postgres}}"
app_db="${POSTGRES_DB:-${POSTGRES_DATABASE:-${POSTGRESQL_DATABASE:-$app_user}}}"
works() { PGPASSWORD="$2" psql -U "$1" -d postgres -c 'select 1' >/dev/null 2>&1; }
if [ -n "$su_pw" ] && works postgres "$su_pw"; then
PGPASSWORD="$su_pw" pg_dumpall -U postgres
elif [ -n "$app_pw" ] && works "$app_user" "$app_pw"; then
if [ "$(PGPASSWORD="$app_pw" psql -U "$app_user" -d postgres -tAc 'select usesuper from pg_user where usename=current_user')" = "t" ]; then
PGPASSWORD="$app_pw" pg_dumpall -U "$app_user"
else
PGPASSWORD="$app_pw" pg_dump -U "$app_user" -d "$app_db"
fi
else
echo "no usable postgres credentials in the pod environment" >&2
exit 1
fi
"#;
fn unavailable(what: &str, out: &crate::host::Output) -> DomainError {
let tail: Vec<&str> = out
.stderr
@ -653,6 +680,24 @@ mod tests {
);
}
#[test]
fn the_dump_command_copes_with_both_postgres_image_families() {
let cmd = POSTGRES_DUMP;
// the official image exposes the superuser and its password directly
assert!(cmd.contains("POSTGRES_PASSWORD"), "{cmd}");
assert!(cmd.contains("POSTGRES_USER"), "{cmd}");
// bitnami keeps them in files and names the superuser password separately
assert!(cmd.contains("POSTGRES_POSTGRES_PASSWORD_FILE"), "{cmd}");
assert!(cmd.contains("POSTGRES_PASSWORD_FILE"), "{cmd}");
// a cluster dump is preferred, with a single-database dump as the fallback
assert!(cmd.contains("pg_dumpall"), "{cmd}");
assert!(cmd.contains("pg_dump -U"), "{cmd}");
// credentials are probed before dumping, so a failure cannot truncate the output
assert!(cmd.contains("psql"), "{cmd}");
// and an unusable database fails loudly instead of writing an empty archive
assert!(cmd.contains("exit 1"), "{cmd}");
}
#[tokio::test]
async fn collector_builds_kubectl_and_tar_commands() {
let r = Arc::new(Rec::default());

217
deploy/nextcloud.yaml Normal file
View File

@ -0,0 +1,217 @@
# Nextcloud on microk8s, published on the host's public port 4444.
# The labels follow the Kubernetes recommended set, so the monitoring app groups the
# deployment and its database into one application on the backup page.
apiVersion: v1
kind: Namespace
metadata:
name: nextcloud
---
apiVersion: v1
kind: Secret
metadata:
name: nextcloud
namespace: nextcloud
type: Opaque
stringData:
POSTGRES_PASSWORD: "__DB_PASSWORD__"
NEXTCLOUD_ADMIN_PASSWORD: "__ADMIN_PASSWORD__"
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nextcloud-postgresql
namespace: nextcloud
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: postgresql
spec:
serviceName: nextcloud-postgresql
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: postgresql
template:
metadata:
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: postgresql
spec:
containers:
- name: postgresql
image: postgres:17-alpine
env:
- name: POSTGRES_DB
value: nextcloud
- name: POSTGRES_USER
value: nextcloud
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
name: postgresql
readinessProbe:
exec:
command: ["pg_isready", "-U", "nextcloud", "-d", "nextcloud"]
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: microk8s-hostpath
resources:
requests:
storage: 8Gi
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud-postgresql
namespace: nextcloud
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: postgresql
spec:
selector:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: postgresql
ports:
- port: 5432
targetPort: postgresql
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nextcloud-data
namespace: nextcloud
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
spec:
accessModes: [ReadWriteOnce]
storageClassName: microk8s-hostpath
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextcloud
namespace: nextcloud
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
template:
metadata:
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
spec:
containers:
- name: nextcloud
image: nextcloud:31-apache
ports:
# published on the host's public interface by the CNI portmap plugin
- containerPort: 80
hostPort: 4444
name: http
env:
- name: POSTGRES_HOST
value: nextcloud-postgresql
- name: POSTGRES_DB
value: nextcloud
- name: POSTGRES_USER
value: nextcloud
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud
key: POSTGRES_PASSWORD
- name: NEXTCLOUD_ADMIN_USER
value: admin
- name: NEXTCLOUD_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: nextcloud
key: NEXTCLOUD_ADMIN_PASSWORD
- name: NEXTCLOUD_TRUSTED_DOMAINS
value: "46.232.248.171:4444 46.232.248.171 localhost"
- name: PHP_MEMORY_LIMIT
value: 512M
- name: PHP_UPLOAD_LIMIT
value: 2G
readinessProbe:
httpGet:
path: /status.php
port: http
httpHeaders:
- name: Host
value: localhost
initialDelaySeconds: 30
periodSeconds: 15
failureThreshold: 20
livenessProbe:
httpGet:
path: /status.php
port: http
httpHeaders:
- name: Host
value: localhost
initialDelaySeconds: 180
periodSeconds: 30
failureThreshold: 6
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
memory: 2Gi
volumeMounts:
- name: data
mountPath: /var/www/html
volumes:
- name: data
persistentVolumeClaim:
claimName: nextcloud-data
---
apiVersion: v1
kind: Service
metadata:
name: nextcloud
namespace: nextcloud
labels:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
spec:
selector:
app.kubernetes.io/instance: nextcloud
app.kubernetes.io/name: nextcloud
ports:
- port: 80
targetPort: http
name: http

View File

@ -14,15 +14,33 @@ Enter the passphrase of the strategy when prompted.
## 2. Restore per source type
### PostgreSQL dump (`.sql.gz`, produced by `pg_dumpall` inside the pod)
### PostgreSQL dump (`.sql.gz`)
The archive is one of two shapes, depending on which credentials the pod exposes. The first
line says which one:
- `-- PostgreSQL database cluster dump` — every database and all roles, taken with the
superuser. Restore it into the cluster.
- `-- PostgreSQL database dump` — a single database, taken with the application user because
the superuser password of the pod did not work. Restore it into that database.
Stop the application first, for example
`microk8s kubectl scale deploy/gitea -n gitea --replicas=0`, and start it again afterwards.
Cluster dump:
```bash
gunzip -c nextcloud-db_*.sql.gz | microk8s kubectl exec -i -n nextcloud nextcloud-postgresql-0 -- \
sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d postgres'
```
Single database:
```bash
gunzip -c gitea-db_*.sql.gz | microk8s kubectl exec -i -n gitea gitea-postgresql-0 -- \
sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres'
sh -c 'PGPASSWORD=$(cat "$POSTGRES_PASSWORD_FILE") psql -U "$POSTGRES_USER" -d "$POSTGRES_DATABASE"'
```
Stop Gitea first (`microk8s kubectl scale deploy/gitea -n gitea --replicas=0`) and start it again afterwards.
### Persistent volume (`.tar.gz` of the hostpath directory)
Find the directory of the PVC on the host and unpack into it: