Skip to content

Commit 50122e4

Browse files
committed
feat(cli): add automated pg_upgrade for PostgreSQL major version upgrades
The `postgresai mon update` command now: - Detects PostgreSQL major version changes (e.g., 15 → 18) - Automatically runs pg_upgrade using the new postgres image - Installs old PG binaries in the new container for upgrade - Uses --link mode for fast, space-efficient upgrades - Preserves metrics data across major version upgrades This allows seamless updates when PostgreSQL version changes in docker-compose.yml without manual intervention or data loss.
1 parent 4a9bca4 commit 50122e4

File tree

1 file changed

+195
-11
lines changed

1 file changed

+195
-11
lines changed

cli/bin/postgres-ai.ts

Lines changed: 195 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,130 @@ function isDockerRunning(): boolean {
10691069
}
10701070
}
10711071

1072+
/**
1073+
* Get PostgreSQL major version from a running container
1074+
* @returns Major version number (e.g., 15, 17, 18) or null if container not running
1075+
*/
1076+
function getRunningPostgresVersion(containerName: string): number | null {
1077+
try {
1078+
const result = spawnSync(
1079+
"docker",
1080+
["exec", containerName, "psql", "-U", "postgres", "-t", "-c", "SHOW server_version_num"],
1081+
{ stdio: "pipe", encoding: "utf8" }
1082+
);
1083+
if (result.status === 0 && result.stdout) {
1084+
const versionNum = parseInt(result.stdout.trim(), 10);
1085+
if (!isNaN(versionNum)) {
1086+
return Math.floor(versionNum / 10000); // e.g., 150000 -> 15
1087+
}
1088+
}
1089+
return null;
1090+
} catch {
1091+
return null;
1092+
}
1093+
}
1094+
1095+
/**
1096+
* Get target PostgreSQL major version from docker-compose.yml
1097+
* @returns Major version number or null if not found
1098+
*/
1099+
function getTargetPostgresVersion(composeFilePath: string): number | null {
1100+
try {
1101+
const content = fs.readFileSync(composeFilePath, "utf8");
1102+
// Match postgres:XX image tag for sink-postgres service
1103+
const match = content.match(/sink-postgres:[\s\S]*?image:\s*postgres:(\d+)/);
1104+
if (match && match[1]) {
1105+
return parseInt(match[1], 10);
1106+
}
1107+
return null;
1108+
} catch {
1109+
return null;
1110+
}
1111+
}
1112+
1113+
/**
1114+
* Run pg_upgrade to migrate PostgreSQL data between major versions
1115+
* Uses the new postgres image with old binaries installed
1116+
*/
1117+
async function runPgUpgrade(
1118+
oldVersion: number,
1119+
newVersion: number,
1120+
projectDir: string
1121+
): Promise<boolean> {
1122+
console.log(`\nMigrating PostgreSQL data from version ${oldVersion} to ${newVersion}...`);
1123+
1124+
const volumeName = "postgres_ai_sink_postgres_data";
1125+
const containerName = "postgres-ai-pg-upgrade";
1126+
1127+
// Build the upgrade script that runs inside the container
1128+
const upgradeScript = `
1129+
set -e
1130+
1131+
echo "Installing PostgreSQL ${oldVersion} binaries..."
1132+
apt-get update -qq
1133+
apt-get install -y -qq postgresql-${oldVersion} >/dev/null 2>&1
1134+
1135+
echo "Preparing data directories..."
1136+
mkdir -p /var/lib/postgresql/${newVersion}/data
1137+
chown postgres:postgres /var/lib/postgresql/${newVersion}/data
1138+
chmod 700 /var/lib/postgresql/${newVersion}/data
1139+
1140+
# Initialize new data directory
1141+
echo "Initializing new PostgreSQL ${newVersion} cluster..."
1142+
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/initdb -D /var/lib/postgresql/${newVersion}/data"
1143+
1144+
# Run pg_upgrade
1145+
echo "Running pg_upgrade..."
1146+
cd /var/lib/postgresql
1147+
su postgres -c "/usr/lib/postgresql/${newVersion}/bin/pg_upgrade \\
1148+
--old-datadir=/var/lib/postgresql/data \\
1149+
--new-datadir=/var/lib/postgresql/${newVersion}/data \\
1150+
--old-bindir=/usr/lib/postgresql/${oldVersion}/bin \\
1151+
--new-bindir=/usr/lib/postgresql/${newVersion}/bin \\
1152+
--link"
1153+
1154+
# Replace old data with upgraded data
1155+
echo "Finalizing upgrade..."
1156+
rm -rf /var/lib/postgresql/data.old 2>/dev/null || true
1157+
mv /var/lib/postgresql/data /var/lib/postgresql/data.old
1158+
mv /var/lib/postgresql/${newVersion}/data /var/lib/postgresql/data
1159+
1160+
echo "PostgreSQL upgrade completed successfully!"
1161+
`;
1162+
1163+
try {
1164+
// Remove any existing upgrade container
1165+
spawnSync("docker", ["rm", "-f", containerName], { stdio: "ignore" });
1166+
1167+
// Run upgrade in a temporary container
1168+
console.log("Starting upgrade container...");
1169+
const result = spawnSync(
1170+
"docker",
1171+
[
1172+
"run",
1173+
"--rm",
1174+
"--name", containerName,
1175+
"-v", `${volumeName}:/var/lib/postgresql/data`,
1176+
`postgres:${newVersion}`,
1177+
"bash", "-c", upgradeScript
1178+
],
1179+
{ stdio: "inherit" }
1180+
);
1181+
1182+
if (result.status === 0) {
1183+
console.log("✓ PostgreSQL upgrade completed successfully\n");
1184+
return true;
1185+
} else {
1186+
console.error("✗ PostgreSQL upgrade failed");
1187+
return false;
1188+
}
1189+
} catch (error) {
1190+
const message = error instanceof Error ? error.message : String(error);
1191+
console.error(`PostgreSQL upgrade failed: ${message}`);
1192+
return false;
1193+
}
1194+
}
1195+
10721196
/**
10731197
* Get docker compose command
10741198
*/
@@ -1660,43 +1784,103 @@ mon
16601784
});
16611785
mon
16621786
.command("update")
1663-
.description("update monitoring stack")
1787+
.description("update monitoring stack (handles PostgreSQL major version upgrades automatically)")
16641788
.action(async () => {
16651789
console.log("Updating PostgresAI monitoring stack...\n");
16661790

1791+
let composeFile: string;
1792+
let projectDir: string;
1793+
16671794
try {
1795+
// Get project directory
1796+
try {
1797+
({ composeFile, projectDir } = await resolveOrInitPaths());
1798+
} catch (error) {
1799+
const message = error instanceof Error ? error.message : String(error);
1800+
console.error(message);
1801+
process.exitCode = 1;
1802+
return;
1803+
}
1804+
16681805
// Check if we're in a git repo
1669-
const gitDir = path.resolve(process.cwd(), ".git");
1806+
const gitDir = path.resolve(projectDir, ".git");
16701807
if (!fs.existsSync(gitDir)) {
16711808
console.error("Not a git repository. Cannot update.");
16721809
process.exitCode = 1;
16731810
return;
16741811
}
16751812

1813+
// Get current PostgreSQL version from running container (before git pull)
1814+
const currentPgVersion = getRunningPostgresVersion("sink-postgres");
1815+
if (currentPgVersion) {
1816+
console.log(`Current PostgreSQL version: ${currentPgVersion}`);
1817+
}
1818+
16761819
// Fetch latest changes
1677-
console.log("Fetching latest changes...");
1678-
await execPromise("git fetch origin");
1820+
console.log("\nFetching latest changes...");
1821+
await execPromise(`git -C "${projectDir}" fetch origin`);
16791822

16801823
// Check current branch
1681-
const { stdout: branch } = await execPromise("git rev-parse --abbrev-ref HEAD");
1824+
const { stdout: branch } = await execPromise(`git -C "${projectDir}" rev-parse --abbrev-ref HEAD`);
16821825
const currentBranch = branch.trim();
16831826
console.log(`Current branch: ${currentBranch}`);
16841827

16851828
// Pull latest changes
16861829
console.log("Pulling latest changes...");
1687-
const { stdout: pullOut } = await execPromise("git pull origin " + currentBranch);
1830+
const { stdout: pullOut } = await execPromise(`git -C "${projectDir}" pull origin ${currentBranch}`);
16881831
console.log(pullOut);
16891832

1833+
// Get target PostgreSQL version from updated docker-compose.yml
1834+
const targetPgVersion = getTargetPostgresVersion(composeFile);
1835+
if (targetPgVersion) {
1836+
console.log(`Target PostgreSQL version: ${targetPgVersion}`);
1837+
}
1838+
1839+
// Check if PostgreSQL major version upgrade is needed
1840+
const needsPgUpgrade = currentPgVersion && targetPgVersion && currentPgVersion !== targetPgVersion;
1841+
1842+
if (needsPgUpgrade) {
1843+
console.log(`\n⚠ PostgreSQL major version change detected: ${currentPgVersion}${targetPgVersion}`);
1844+
1845+
// Stop services before upgrade
1846+
console.log("\nStopping services for PostgreSQL upgrade...");
1847+
await runCompose(["stop"]);
1848+
1849+
// Run pg_upgrade
1850+
const upgradeSuccess = await runPgUpgrade(currentPgVersion, targetPgVersion, projectDir);
1851+
1852+
if (!upgradeSuccess) {
1853+
console.error("\n✗ PostgreSQL upgrade failed");
1854+
console.error("Your data is preserved in the original format.");
1855+
console.error("You can either:");
1856+
console.error(" 1. Fix the issue and run 'postgres-ai mon update' again");
1857+
console.error(" 2. Reset the database: postgres-ai mon reset sink-postgres");
1858+
process.exitCode = 1;
1859+
return;
1860+
}
1861+
}
1862+
16901863
// Update Docker images
1691-
console.log("\nUpdating Docker images...");
1864+
console.log("Updating Docker images...");
16921865
const code = await runCompose(["pull"]);
16931866

1694-
if (code === 0) {
1867+
if (code !== 0) {
1868+
console.error("\n✗ Docker image update failed");
1869+
process.exitCode = 1;
1870+
return;
1871+
}
1872+
1873+
// Restart services to apply updates
1874+
console.log("\nRestarting services...");
1875+
const restartCode = await runCompose(["up", "-d", "--force-recreate"]);
1876+
1877+
if (restartCode === 0) {
16951878
console.log("\n✓ Update completed successfully");
1696-
console.log("\nTo apply updates, restart monitoring services:");
1697-
console.log(" postgres-ai mon restart");
1879+
if (needsPgUpgrade) {
1880+
console.log(`✓ PostgreSQL upgraded from ${currentPgVersion} to ${targetPgVersion}`);
1881+
}
16981882
} else {
1699-
console.error("\n✗ Docker image update failed");
1883+
console.error("\n✗ Failed to restart services");
17001884
process.exitCode = 1;
17011885
}
17021886
} catch (error) {

0 commit comments

Comments
 (0)