diff --git a/backend/crates/infrastructure/src/backup.rs b/backend/crates/infrastructure/src/backup.rs index 37ed518..75e908d 100644 --- a/backend/crates/infrastructure/src/backup.rs +++ b/backend/crates/infrastructure/src/backup.rs @@ -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()); diff --git a/deploy/nextcloud.yaml b/deploy/nextcloud.yaml new file mode 100644 index 0000000..b38509d --- /dev/null +++ b/deploy/nextcloud.yaml @@ -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 diff --git a/docs/restore.md b/docs/restore.md index 595e551..e57fdd0 100644 --- a/docs/restore.md +++ b/docs/restore.md @@ -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: