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:
@ -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
217
deploy/nextcloud.yaml
Normal 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
|
||||
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user