Files
uptime-kuma/extra/prepare-v2-test-db.mjs
T

141 lines
5.5 KiB
JavaScript

/**
* Regenerate test/fixtures/v2-kuma.db, a pristine Uptime Kuma 2.5.3 SQLite database.
*
* The migration test (test/backend-test/test-migration-v2-to-v3.ts) upgrades this real
* 2.5.3 database to v3 to make sure the upgrade path keeps working (see #7944).
*
* It is generated by booting the official 2.5.3 slim image once with an empty data dir,
* letting it create the database, then checkpointing the WAL so the file is standalone.
*
* Usage:
* node extra/prepare-v2-test-db.mjs
*
* Requires Docker. Only run this when the v2 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/v2-kuma.db.gz");
const image = "louislam/uptime-kuma:2.5.3-slim";
const containerName = "uptime-kuma-v2-fixture";
const adminPassword = "Kuma-V2-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-v2-fixture-"));
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v2-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 created and migrated the database
const dbPath = path.join(dataDir, "kuma.db");
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
if (fs.existsSync(dbPath) && fs.statSync(dbPath).size > 0) {
const walPath = `${dbPath}-wal`;
if (fs.existsSync(walPath) && fs.statSync(walPath).size > 0) {
break;
}
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
if (!fs.existsSync(dbPath)) {
throw new Error("Timed out waiting for the v2 database to be created");
}
run("docker", ["stop", containerName]);
// The 2.5.3 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 2.5.3 dataset (an admin with one of each owned resource), so the fixture
// can be used to boot the server directly.
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 remote_browser (name, url, user_id) VALUES ('Browser 1', 'http://127.0.0.1:9222', 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 });
}