Production readiness hardening and ops tooling
This commit is contained in:
85
scripts/backup-production.mjs
Normal file
85
scripts/backup-production.mjs
Normal file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import "dotenv/config";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArg(name) {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] : undefined;
|
||||
}
|
||||
|
||||
function hasFlag(name) {
|
||||
return args.includes(name);
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
node scripts/backup-production.mjs [--out ./backups] [--include-mosquitto] [--dry-run]
|
||||
|
||||
Creates a timestamped PostgreSQL custom-format dump. When --include-mosquitto is set,
|
||||
copies Mosquitto passwd/ACL files if readable.
|
||||
`);
|
||||
}
|
||||
|
||||
if (hasFlag("--help") || hasFlag("-h")) {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const outDir = path.resolve(process.cwd(), getArg("--out") || process.env.BACKUP_DIR || "./backups");
|
||||
const includeMosquitto = hasFlag("--include-mosquitto");
|
||||
const dryRun = hasFlag("--dry-run");
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const dbName = process.env.PGDATABASE || "qris_soundbox_platform";
|
||||
const dbBackup = path.join(outDir, `${dbName}-${timestamp}.dump`);
|
||||
const passwdFile = process.env.MOSQUITTO_PASSWD_FILE || "/etc/mosquitto/passwd";
|
||||
const aclFile = process.env.MOSQUITTO_ACL_FILE || "/etc/mosquitto/acl";
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const pgDumpArgs = ["-Fc", "-f", dbBackup];
|
||||
if (process.env.DATABASE_URL) {
|
||||
pgDumpArgs.push(process.env.DATABASE_URL);
|
||||
} else {
|
||||
pgDumpArgs.push("-h", process.env.PGHOST || "127.0.0.1");
|
||||
pgDumpArgs.push("-p", String(process.env.PGPORT || 5432));
|
||||
pgDumpArgs.push("-U", process.env.PGUSER || "postgres");
|
||||
pgDumpArgs.push(dbName);
|
||||
}
|
||||
|
||||
const plan = {
|
||||
out_dir: outDir,
|
||||
database_backup: dbBackup,
|
||||
include_mosquitto: includeMosquitto,
|
||||
commands: [`pg_dump ${pgDumpArgs.map((arg) => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" ")}`]
|
||||
};
|
||||
|
||||
if (dryRun) {
|
||||
console.log(JSON.stringify({ dry_run: true, ...plan }, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = spawnSync("pg_dump", pgDumpArgs, {
|
||||
stdio: "inherit",
|
||||
env: process.env
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`pg_dump failed with status ${result.status}`);
|
||||
}
|
||||
|
||||
const copied = [];
|
||||
if (includeMosquitto) {
|
||||
for (const source of [passwdFile, aclFile]) {
|
||||
if (!fs.existsSync(source)) {
|
||||
continue;
|
||||
}
|
||||
const target = path.join(outDir, `${path.basename(source)}-${timestamp}`);
|
||||
fs.copyFileSync(source, target);
|
||||
copied.push({ source, target });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ ok: true, ...plan, copied }, null, 2));
|
||||
Reference in New Issue
Block a user