mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-21 16:25:19 +00:00
@@ -23,6 +23,7 @@ import m14 from "./scriptsPg/1.15.4";
|
|||||||
import m15 from "./scriptsPg/1.16.0";
|
import m15 from "./scriptsPg/1.16.0";
|
||||||
import m16 from "./scriptsPg/1.17.0";
|
import m16 from "./scriptsPg/1.17.0";
|
||||||
import m17 from "./scriptsPg/1.18.0";
|
import m17 from "./scriptsPg/1.18.0";
|
||||||
|
import m18 from "./scriptsPg/1.18.3";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -45,7 +46,8 @@ const migrations = [
|
|||||||
{ version: "1.15.4", run: m14 },
|
{ version: "1.15.4", run: m14 },
|
||||||
{ version: "1.16.0", run: m15 },
|
{ version: "1.16.0", run: m15 },
|
||||||
{ version: "1.17.0", run: m16 },
|
{ version: "1.17.0", run: m16 },
|
||||||
{ version: "1.18.0", run: m17 }
|
{ version: "1.18.0", run: m17 },
|
||||||
|
{ version: "1.18.3", run: m18 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import m35 from "./scriptsSqlite/1.15.4";
|
|||||||
import m36 from "./scriptsSqlite/1.16.0";
|
import m36 from "./scriptsSqlite/1.16.0";
|
||||||
import m37 from "./scriptsSqlite/1.17.0";
|
import m37 from "./scriptsSqlite/1.17.0";
|
||||||
import m38 from "./scriptsSqlite/1.18.0";
|
import m38 from "./scriptsSqlite/1.18.0";
|
||||||
|
import m39 from "./scriptsSqlite/1.18.3";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -79,7 +80,8 @@ const migrations = [
|
|||||||
{ version: "1.15.4", run: m35 },
|
{ version: "1.15.4", run: m35 },
|
||||||
{ version: "1.16.0", run: m36 },
|
{ version: "1.16.0", run: m36 },
|
||||||
{ version: "1.17.0", run: m37 },
|
{ version: "1.17.0", run: m37 },
|
||||||
{ version: "1.18.0", run: m38 }
|
{ version: "1.18.0", run: m38 },
|
||||||
|
{ version: "1.18.3", run: m39 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
173
server/setup/scriptsPg/1.18.3.ts
Normal file
173
server/setup/scriptsPg/1.18.3.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
const version = "1.18.3";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
// Query existing targetHealthCheck data with joined siteId and orgId before
|
||||||
|
// the transaction adds the new columns (which start NULL for existing rows).
|
||||||
|
// We will delete all rows and reinsert them with targetHealthCheckId = targetId
|
||||||
|
// so the two IDs form a stable 1:1 mapping.
|
||||||
|
const healthChecksQuery = await db.execute(
|
||||||
|
sql`SELECT
|
||||||
|
thc."targetHealthCheckId",
|
||||||
|
thc."targetId",
|
||||||
|
t."siteId",
|
||||||
|
s."orgId",
|
||||||
|
r."name" AS "resourceName",
|
||||||
|
t."ip",
|
||||||
|
t."port"
|
||||||
|
FROM "targetHealthCheck" thc
|
||||||
|
JOIN "targets" t ON thc."targetId" = t."targetId"
|
||||||
|
JOIN "sites" s ON t."siteId" = s."siteId"
|
||||||
|
JOIN "resources" r ON t."resourceId" = r."resourceId"
|
||||||
|
WHERE thc."name" IS NULL OR thc."name" = ''`
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingHealthChecks = healthChecksQuery.rows as {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
targetId: number;
|
||||||
|
siteId: number;
|
||||||
|
orgId: string;
|
||||||
|
resourceName: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${existingHealthChecks.length} existing targetHealthCheck row(s) to migrate`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "trialNotifications" (
|
||||||
|
"notificationId" serial PRIMARY KEY NOT NULL,
|
||||||
|
"subscriptionId" varchar(255) NOT NULL,
|
||||||
|
"notificationType" varchar(50) NOT NULL,
|
||||||
|
"sentAt" bigint NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "trialNotifications" ADD CONSTRAINT "trialNotifications_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingHealthChecks.length > 0) {
|
||||||
|
// fix the name column
|
||||||
|
try {
|
||||||
|
for (const hc of existingHealthChecks) {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "targetHealthCheck"
|
||||||
|
SET "name" = ${`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`}
|
||||||
|
WHERE "targetHealthCheckId" = ${hc.targetHealthCheckId}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error while migrating targetHealthCheck rows:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute resource health by aggregating across the resource's targets'
|
||||||
|
// target health checks, then update the resources.health column to match.
|
||||||
|
try {
|
||||||
|
const resourceTargetHealthQuery = await db.execute(
|
||||||
|
sql`SELECT
|
||||||
|
r."resourceId" AS "resourceId",
|
||||||
|
r."orgId" AS "orgId",
|
||||||
|
r."health" AS "currentHealth",
|
||||||
|
thc."hcHealth" AS "hcHealth"
|
||||||
|
FROM "resources" r
|
||||||
|
LEFT JOIN "targets" t ON t."resourceId" = r."resourceId"
|
||||||
|
LEFT JOIN "targetHealthCheck" thc ON thc."targetId" = t."targetId"`
|
||||||
|
);
|
||||||
|
const resourceTargetHealthRows = resourceTargetHealthQuery.rows as {
|
||||||
|
resourceId: number;
|
||||||
|
orgId: string;
|
||||||
|
currentHealth: string | null;
|
||||||
|
hcHealth: string | null;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const resourceHealthMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
hasHealthy: boolean;
|
||||||
|
hasUnhealthy: boolean;
|
||||||
|
hasUnknown: boolean;
|
||||||
|
orgId: string;
|
||||||
|
currentHealth: string | null;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
for (const row of resourceTargetHealthRows) {
|
||||||
|
const entry = resourceHealthMap.get(row.resourceId) ?? {
|
||||||
|
hasHealthy: false,
|
||||||
|
hasUnhealthy: false,
|
||||||
|
hasUnknown: false,
|
||||||
|
orgId: row.orgId,
|
||||||
|
currentHealth: row.currentHealth
|
||||||
|
};
|
||||||
|
const status = row.hcHealth ?? "unknown";
|
||||||
|
if (status === "healthy") entry.hasHealthy = true;
|
||||||
|
else if (status === "unhealthy") entry.hasUnhealthy = true;
|
||||||
|
else entry.hasUnknown = true;
|
||||||
|
resourceHealthMap.set(row.resourceId, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
let updatedResourceCount = 0;
|
||||||
|
for (const [resourceId, entry] of resourceHealthMap.entries()) {
|
||||||
|
let aggregated: "healthy" | "unhealthy" | "degraded" | "unknown";
|
||||||
|
if (entry.hasHealthy && entry.hasUnhealthy) {
|
||||||
|
aggregated = "degraded";
|
||||||
|
} else if (entry.hasHealthy) {
|
||||||
|
aggregated = "healthy";
|
||||||
|
} else if (entry.hasUnhealthy) {
|
||||||
|
aggregated = "unhealthy";
|
||||||
|
} else {
|
||||||
|
aggregated = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.currentHealth !== aggregated) {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "resources"
|
||||||
|
SET "health" = ${aggregated}
|
||||||
|
WHERE "resourceId" = ${resourceId}
|
||||||
|
`);
|
||||||
|
await db.execute(sql`
|
||||||
|
INSERT INTO "statusHistory" ("entityType", "entityId", "orgId", "status", "timestamp")
|
||||||
|
VALUES ('resource', ${resourceId}, ${entry.orgId}, ${aggregated}, ${now})
|
||||||
|
`);
|
||||||
|
updatedResourceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Recomputed health for ${updatedResourceCount} resource(s) based on target health checks`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
"Error while recomputing resource health from target health checks:",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
172
server/setup/scriptsSqlite/1.18.3.ts
Normal file
172
server/setup/scriptsSqlite/1.18.3.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.18.3";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE 'trialNotifications' (
|
||||||
|
'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
'subscriptionId' text NOT NULL,
|
||||||
|
'notificationType' text NOT NULL,
|
||||||
|
'sentAt' integer NOT NULL,
|
||||||
|
FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
console.log("Migrated database");
|
||||||
|
|
||||||
|
// Fix names for health checks that don't have one
|
||||||
|
const healthChecksWithoutName = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT
|
||||||
|
thc."targetHealthCheckId",
|
||||||
|
r."name" AS "resourceName",
|
||||||
|
t."ip",
|
||||||
|
t."port"
|
||||||
|
FROM 'targetHealthCheck' thc
|
||||||
|
JOIN 'targets' t ON thc."targetId" = t."targetId"
|
||||||
|
JOIN 'resources' r ON t."resourceId" = r."resourceId"
|
||||||
|
WHERE thc."name" IS NULL OR thc."name" = ''`
|
||||||
|
)
|
||||||
|
.all() as {
|
||||||
|
targetHealthCheckId: number;
|
||||||
|
resourceName: string;
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${healthChecksWithoutName.length} targetHealthCheck row(s) with missing names`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (healthChecksWithoutName.length > 0) {
|
||||||
|
const updateName = db.prepare(
|
||||||
|
`UPDATE 'targetHealthCheck' SET "name" = ? WHERE "targetHealthCheckId" = ?`
|
||||||
|
);
|
||||||
|
const updateAllNames = db.transaction(() => {
|
||||||
|
for (const hc of healthChecksWithoutName) {
|
||||||
|
updateName.run(
|
||||||
|
`Resource ${hc.resourceName} - ${hc.ip}:${hc.port}`,
|
||||||
|
hc.targetHealthCheckId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateAllNames();
|
||||||
|
console.log(
|
||||||
|
`Updated names for ${healthChecksWithoutName.length} targetHealthCheck row(s)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute resource health by aggregating across the resource's
|
||||||
|
// targets' target health checks, then update resources.health and
|
||||||
|
// insert a statusHistory entry for any resource whose health changed.
|
||||||
|
const resourceTargetHealthRows = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT
|
||||||
|
r."resourceId" AS "resourceId",
|
||||||
|
r."orgId" AS "orgId",
|
||||||
|
r."health" AS "currentHealth",
|
||||||
|
thc."hcHealth" AS "hcHealth"
|
||||||
|
FROM 'resources' r
|
||||||
|
LEFT JOIN 'targets' t ON t."resourceId" = r."resourceId"
|
||||||
|
LEFT JOIN 'targetHealthCheck' thc ON thc."targetId" = t."targetId"`
|
||||||
|
)
|
||||||
|
.all() as {
|
||||||
|
resourceId: number;
|
||||||
|
orgId: string;
|
||||||
|
currentHealth: string | null;
|
||||||
|
hcHealth: string | null;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const resourceHealthMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
hasHealthy: boolean;
|
||||||
|
hasUnhealthy: boolean;
|
||||||
|
hasUnknown: boolean;
|
||||||
|
orgId: string;
|
||||||
|
currentHealth: string | null;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
for (const row of resourceTargetHealthRows) {
|
||||||
|
const entry = resourceHealthMap.get(row.resourceId) ?? {
|
||||||
|
hasHealthy: false,
|
||||||
|
hasUnhealthy: false,
|
||||||
|
hasUnknown: false,
|
||||||
|
orgId: row.orgId,
|
||||||
|
currentHealth: row.currentHealth
|
||||||
|
};
|
||||||
|
const status = row.hcHealth ?? "unknown";
|
||||||
|
if (status === "healthy") entry.hasHealthy = true;
|
||||||
|
else if (status === "unhealthy") entry.hasUnhealthy = true;
|
||||||
|
else entry.hasUnknown = true;
|
||||||
|
resourceHealthMap.set(row.resourceId, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResourceHealth = db.prepare(
|
||||||
|
`UPDATE 'resources' SET "health" = ? WHERE "resourceId" = ?`
|
||||||
|
);
|
||||||
|
const insertResourceHistory = db.prepare(
|
||||||
|
`INSERT INTO 'statusHistory' ("entityType", "entityId", "orgId", "status", "timestamp") VALUES (?, ?, ?, ?, ?)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
let updatedResourceCount = 0;
|
||||||
|
|
||||||
|
const recomputeAll = db.transaction(() => {
|
||||||
|
for (const [resourceId, entry] of resourceHealthMap.entries()) {
|
||||||
|
let aggregated:
|
||||||
|
| "healthy"
|
||||||
|
| "unhealthy"
|
||||||
|
| "degraded"
|
||||||
|
| "unknown";
|
||||||
|
if (entry.hasHealthy && entry.hasUnhealthy) {
|
||||||
|
aggregated = "degraded";
|
||||||
|
} else if (entry.hasHealthy) {
|
||||||
|
aggregated = "healthy";
|
||||||
|
} else if (entry.hasUnhealthy) {
|
||||||
|
aggregated = "unhealthy";
|
||||||
|
} else {
|
||||||
|
aggregated = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.currentHealth !== aggregated) {
|
||||||
|
updateResourceHealth.run(aggregated, resourceId);
|
||||||
|
insertResourceHistory.run(
|
||||||
|
"resource",
|
||||||
|
resourceId,
|
||||||
|
entry.orgId,
|
||||||
|
aggregated,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
updatedResourceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
recomputeAll();
|
||||||
|
console.log(
|
||||||
|
`Recomputed health for ${updatedResourceCount} resource(s) based on target health checks`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user