mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Merge remote-tracking branch 'origin/release/v1.46.0' into fix/v146-move-safety
This commit is contained in:
commit
0085915a56
53 files changed
+1620
-246
No files matched your search
@@ -6,6 +6,10 @@
|
||||
# 1. Build the app image from this checkout and start the bundled compose
|
||||
# stack with fixed throwaway keys, and an S3-compatible bucket outside
|
||||
# it (Versity S3 Gateway, pinned) as the off-host target.
|
||||
# First without the `backups` volume: the run must keep nothing in the
|
||||
# container's layer, say so in its ledger row and the start log, and
|
||||
# still upload the dump. Then the stack is thrown away and started again
|
||||
# as shipped.
|
||||
# 2. Seed real data: the demo seed (documents, encrypted notes, years of
|
||||
# readings) and a Guardian with a managed profile, written by the app's
|
||||
# own code; then, through the API, a reading with an encrypted note.
|
||||
@@ -184,6 +188,36 @@ docker run -d --name "$BUCKET" --network "$BUCKET" -p "127.0.0.1:$BUCKET_PORT:70
|
||||
"mkdir -p /data/$BACKUP_S3_BUCKET && exec versitygw --access $BACKUP_S3_ACCESS_KEY --secret $BACKUP_S3_SECRET_KEY posix /data" \
|
||||
>/dev/null || fail "the bucket did not start"
|
||||
|
||||
step "a server without the backups volume keeps no dump on itself"
|
||||
# The image creates the dump directory, so without the volume it is writable,
|
||||
# and it sits in the container's layer that the next update throws away. No
|
||||
# dump may be kept, or counted as kept, there; the bucket still gets one.
|
||||
cat >"$WORK/no-volume.yml" <<YAML
|
||||
services:
|
||||
app:
|
||||
volumes: !reset []
|
||||
YAML
|
||||
stack -f "$WORK/no-volume.yml" up -d app >"$WORK/up.log" 2>&1 ||
|
||||
{ tail -20 "$WORK/up.log" >&2; fail "the stack without the volume did not start"; }
|
||||
wait_healthy
|
||||
i=0
|
||||
until RUN=$(sql "SELECT status || ' ' || local_result || ' ' || offhost_result FROM database_backup_runs WHERE status <> 'running' ORDER BY started_at DESC LIMIT 1") && [ -n "$RUN" ]; do
|
||||
i=$((i + 1))
|
||||
[ "$i" -lt 120 ] || fail "no database-backup run within four minutes without the volume"
|
||||
sleep 2
|
||||
done
|
||||
[ "$RUN" = "succeeded not_persistent uploaded" ] ||
|
||||
fail "without the volume the run read '$RUN', not 'succeeded not_persistent uploaded'"
|
||||
LEFT=$(docker exec "$PROJECT-app" sh -c 'ls -A /var/lib/healthlog/backups')
|
||||
[ -z "$LEFT" ] || fail "a dump was left in the container's layer: $LEFT"
|
||||
# Into a file first: `grep -q` stops reading at the match, and with pipefail
|
||||
# the SIGPIPE that leaves `docker logs` would read as a miss.
|
||||
docker logs "$PROJECT-app" >"$WORK/no-volume.log" 2>&1
|
||||
grep -q "is inside the container, not on a mounted volume" "$WORK/no-volume.log" ||
|
||||
fail "the start log does not say the dump directory is inside the container"
|
||||
echo " $RUN, nothing in the container's layer"
|
||||
stack down -v --remove-orphans >/dev/null 2>&1
|
||||
|
||||
step "a running server with real data"
|
||||
stack up -d app >"$WORK/up.log" 2>&1 || { tail -20 "$WORK/up.log" >&2; fail "the stack did not start"; }
|
||||
wait_healthy
|
||||
|
||||
+19
-7
@@ -13,9 +13,11 @@
|
||||
*
|
||||
* Exit 0: the dump was written, or there was nothing to do (no pending
|
||||
* migration, a new database, `DB_BACKUP_BEFORE_MIGRATE=false`), or there is no
|
||||
* writable directory to put it in, which is said as a warning and is not a
|
||||
* reason to keep the app down. Exit 1: the directory is writable and the dump
|
||||
* failed. The entrypoint then stops the boot and names the switch that skips
|
||||
* directory to put it in that survives the container (none writable, or only
|
||||
* the image's own directory without the volume, which the update that brings
|
||||
* these migrations is about to throw away), which is said as a warning and is
|
||||
* not a reason to keep the app down. Exit 1: the directory is kept and the
|
||||
* dump failed. The entrypoint then stops the boot and names the switch that skips
|
||||
* the dump, because migrating without the way back the operator was promised
|
||||
* is the one thing this step exists to prevent.
|
||||
*
|
||||
@@ -28,9 +30,9 @@ import { pathToFileURL } from "node:url";
|
||||
|
||||
import {
|
||||
applyRetention,
|
||||
backupDirectoryStatus,
|
||||
connect,
|
||||
createDatabaseDump,
|
||||
isWritableDirectory,
|
||||
latestMigration,
|
||||
listUserTables,
|
||||
connectionFromUrl,
|
||||
@@ -60,7 +62,11 @@ function mb(bytes) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export async function preMigrationDump(env = process.env) {
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} [env]
|
||||
* @param {{ directoryStatus?: typeof backupDirectoryStatus }} [options]
|
||||
*/
|
||||
export async function preMigrationDump(env = process.env, options = {}) {
|
||||
if (!preMigrationDumpEnabled(env)) {
|
||||
log("DB_BACKUP_BEFORE_MIGRATE=false, no dump before migrating.");
|
||||
return 0;
|
||||
@@ -101,9 +107,15 @@ export async function preMigrationDump(env = process.env) {
|
||||
}
|
||||
|
||||
const dir = resolveBackupDir(env);
|
||||
if (!isWritableDirectory(dir)) {
|
||||
const status = (options.directoryStatus ?? backupDirectoryStatus)(dir);
|
||||
if (!status.persistent) {
|
||||
const why = !status.writable
|
||||
? "is not writable"
|
||||
: status.reason === "memory"
|
||||
? "is in memory (tmpfs) and would not keep a dump past a restart"
|
||||
: "is inside the container, not on a mounted volume, and would not keep a dump past this update";
|
||||
warn(
|
||||
`${dir} is not writable, so no dump is taken before the ${pending.length} pending migration(s). ` +
|
||||
`${dir} ${why}, so no dump is taken before the ${pending.length} pending migration(s). ` +
|
||||
"Mount the backups volume (docker-compose.yml) to keep a way back; continuing.",
|
||||
);
|
||||
return 0;
|
||||
|
||||
@@ -260,11 +260,16 @@
|
||||
"variables": [
|
||||
{
|
||||
"name": "DEPLOY_WEBHOOK_SECRET",
|
||||
"purpose": "Shared secret for the X-Deploy-Webhook-Secret header on POST /api/internal/deploy-webhook. 64 hex chars."
|
||||
"purpose": "Shared secret of POST /api/internal/deploy-webhook. 64 hex chars. A request authenticates by sending it in the X-Deploy-Webhook-Secret header (Coolify's stock sender; no replay protection), or by signing: X-Deploy-Webhook-Signature: sha256=<HMAC-SHA256 under this secret of the X-Deploy-Webhook-Timestamp value, a dot and the raw body>, the timestamp within 5 minutes."
|
||||
},
|
||||
{
|
||||
"name": "DEPLOY_LOGS_URL",
|
||||
"purpose": "URL of the deployment dashboard (e.g. the Coolify UI) linked in deploy-failure notifications. Optional; when unset the notification omits the link."
|
||||
},
|
||||
{
|
||||
"name": "DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP",
|
||||
"purpose": "\"true\" (or \"1\") accepts only signed deploy-webhook requests (X-Deploy-Webhook-Signature with a fresh X-Deploy-Webhook-Timestamp); the secret header alone is refused with 401. Leave unset for Coolify's stock sender, which can only send the secret header.",
|
||||
"example": "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -372,7 +377,7 @@
|
||||
},
|
||||
{
|
||||
"name": "DB_BACKUP_DIR",
|
||||
"purpose": "Directory the dumps are written to and restored from. Default /var/lib/healthlog/backups, the `backups` volume. Without a writable directory the nightly dump only goes off-host and no dump is taken before migrations.",
|
||||
"purpose": "Directory the dumps are written to and restored from. Default /var/lib/healthlog/backups, the `backups` volume. In a container it must be a mount of its own: a directory in the container's layer is lost at the next update and counts as none. Without one the nightly dump only goes off-host and no dump is taken before migrations.",
|
||||
"example": "/var/lib/healthlog/backups"
|
||||
},
|
||||
{
|
||||
@@ -424,7 +429,12 @@
|
||||
},
|
||||
{
|
||||
"name": "TRUST_PROXY_HOPS",
|
||||
"purpose": "Trusted reverse-proxy hop count; the client IP is read that many entries from the right of X-Forwarded-For.",
|
||||
"purpose": "Trusted reverse-proxy hop count; the client IP is read that many entries from the right of X-Forwarded-For. Forwarding headers are read only from a peer whose own address is not public (loopback, private, CGNAT, unique-local, link-local, reserved, multicast).",
|
||||
"example": "1"
|
||||
},
|
||||
{
|
||||
"name": "TRUST_CF_CONNECTING_IP",
|
||||
"purpose": "\"1\" takes the client IP from Cloudflare's CF-Connecting-IP header, from every connection and before the peer check. Safe only when the origin accepts connections from Cloudflare alone (a Tunnel, or a firewall admitting only Cloudflare's ranges); otherwise any caller forges the address rate limits and the audit log record. Off by default.",
|
||||
"example": "1"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user