Resourcemanagement/extra/prepare-v1-test-db.mjs

143 lines
5.6 KiB
JavaScript

/**
* Regenerate test/fixtures/v1-kuma.db.gz, a pristine Uptime Kuma 1.23.17 SQLite database.
*
* The migration test (test/backend-test/test-migration-v1-to-v3.ts) upgrades this real
* 1.23.17 database straight to v3 to make sure the v1 -> v3 upgrade path keeps working.
*
* It is generated by booting the official 1.23.17 image once with an empty data dir,
* letting it create and patch the database, then checkpointing the WAL so the file is standalone.
*
* Usage:
* node extra/prepare-v1-test-db.mjs
*
* Requires Docker. Only run this when the v1 schema actually changed.
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import zlib from "node:zlib";
import { fileURLToPath } from "node:url";
import BetterSqlite3 from "better-sqlite3";
import bcrypt from "bcryptjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.join(__dirname, "..");
const fixturePath = path.join(repoRoot, "test/fixtures/v1-kuma.db.gz");
const image = "louislam/uptime-kuma:1.23.17";
const containerName = "uptime-kuma-v1-fixture";
const adminPassword = "Kuma-V1-Test-Password";
/**
* Run a command and inherit stdio.
* @param {string} command Command to run
* @param {string[]} args Arguments
* @returns {void}
*/
function run(command, args) {
console.log(`$ ${command} ${args.join(" ")}`);
execFileSync(command, args, { stdio: "inherit" });
}
/**
* Copy a file if it exists.
* @param {string} from Source
* @param {string} to Destination
* @returns {void}
*/
function copyIfExists(from, to) {
if (fs.existsSync(from)) {
fs.copyFileSync(from, to);
}
}
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v1-fixture-"));
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v1-work-"));
try {
run("docker", ["rm", "-f", containerName]);
run("docker", [
"run",
"-d",
"--name",
containerName,
"-e",
"UPTIME_KUMA_DB_TYPE=sqlite",
"-v",
`${dataDir}:/app/data`,
image,
]);
// Wait until the container finished patching the database and started the server
const dbPath = path.join(dataDir, "kuma.db");
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
let logs = "";
try {
logs = execFileSync("docker", ["logs", containerName], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
} catch (e) {
logs = "";
}
if (logs.includes("Database Patched Successfully") && fs.existsSync(dbPath) && fs.statSync(dbPath).size > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
if (!fs.existsSync(dbPath)) {
throw new Error("Timed out waiting for the v1 database to be created");
}
run("docker", ["stop", containerName]);
// Like v2, the image keeps the schema in the WAL file, so copy all three files and checkpoint
copyIfExists(dbPath, path.join(workDir, "kuma.db"));
copyIfExists(`${dbPath}-wal`, path.join(workDir, "kuma.db-wal"));
copyIfExists(`${dbPath}-shm`, path.join(workDir, "kuma.db-shm"));
const db = new BetterSqlite3(path.join(workDir, "kuma.db"));
db.pragma("wal_checkpoint(TRUNCATE)");
// Seed a realistic v1 dataset (an admin with one of each owned resource), so the fixture
// can be used to boot the server directly. v1 has no remote_browser table yet; the v3
// migrations create it during the upgrade.
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
const adminPasswordHash = await bcrypt.hash(adminPassword, 10);
db.prepare("INSERT INTO user (username, password, active, timezone, twofa_status) VALUES (?, ?, 1, 'UTC', 0)").run(
"admin",
adminPasswordHash
);
db.prepare(
"INSERT INTO monitor (name, active, user_id, interval, type, url, method, maxretries, maxredirects, accepted_statuscodes_json, retry_interval, created_date) VALUES ('Migrated Monitor', 0, 1, 60, 'http', 'https://example.com', 'GET', 0, 10, '[\"200-299\"]', 60, ?)"
).run(now);
db.prepare("INSERT INTO notification (name, active, user_id, is_default, config) VALUES ('Notif 1', 1, 1, 0, '{}')").run();
db.prepare(
"INSERT INTO maintenance (title, description, user_id, active, strategy, weekdays, days_of_month) VALUES ('Maintenance 1', '', 1, 1, 'manual', '[]', '[]')"
).run();
db.prepare(
"INSERT INTO proxy (user_id, protocol, host, port, auth, active, `default`, created_date) VALUES (1, 'http', '127.0.0.1', 8080, 0, 1, 0, ?)"
).run(now);
db.prepare("INSERT INTO docker_host (user_id, docker_daemon, docker_type, name) VALUES (1, 'tcp://127.0.0.1:2375', 'socket', 'Docker 1')").run();
db.prepare("INSERT INTO api_key (key, name, user_id, created_date, active) VALUES (?, 'API Key 1', 1, ?, 1)").run(
await bcrypt.hash("test-api-key", 10),
now
);
// Keep the fixture compact and in rollback-journal mode so simply opening it does not create
// -wal/-shm sidecar files. Set VACUUM for a compact file, then it is gzipped before being saved.
db.pragma("journal_mode = DELETE");
db.exec("VACUUM");
db.close();
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
fs.writeFileSync(fixturePath, zlib.gzipSync(fs.readFileSync(path.join(workDir, "kuma.db")), { level: 9 }));
console.log(`Wrote ${fixturePath}`);
} finally {
try {
run("docker", ["rm", "-f", containerName]);
} catch (e) {
// Ignore
}
fs.rmSync(dataDir, { recursive: true, force: true });
fs.rmSync(workDir, { recursive: true, force: true });
}