convex
Version:
Client for the Convex Cloud
273 lines (260 loc) • 7.52 kB
text/typescript
import path from "path";
import {
Context,
logFailure,
logFinishedStep,
logVerbose,
} from "../../../bundler/context.js";
import { runQuery } from "../run.js";
import { deploymentStateDir, saveDeploymentConfig } from "./filePaths.js";
import {
ensureBackendBinaryDownloaded,
ensureBackendStopped,
localDeploymentUrl,
runLocalBackend,
} from "./run.js";
import {
downloadSnapshotExport,
startSnapshotExport,
} from "../../convexExport.js";
import { deploymentFetch, logAndHandleFetchError } from "../utils/utils.js";
import {
confirmImport,
uploadForImport,
waitForStableImportState,
} from "../../convexImport.js";
import { promptOptions, promptYesNo } from "../utils/prompts.js";
import { recursivelyDelete } from "../fsUtils.js";
export async function handlePotentialUpgrade(
ctx: Context,
args: {
deploymentName: string;
oldVersion: string | null;
newBinaryPath: string;
newVersion: string;
ports: {
cloud: number;
site: number;
};
adminKey: string;
forceUpgrade: boolean;
},
): Promise<{ cleanupHandle: () => Promise<void> }> {
const newConfig = {
ports: args.ports,
backendVersion: args.newVersion,
adminKey: args.adminKey,
};
if (args.oldVersion === null || args.oldVersion === args.newVersion) {
// No upgrade needed. Save the current config and start running the backend.
saveDeploymentConfig(ctx, args.deploymentName, newConfig);
return runLocalBackend(ctx, {
binaryPath: args.newBinaryPath,
deploymentName: args.deploymentName,
ports: args.ports,
});
}
const confirmed =
args.forceUpgrade ||
(await promptYesNo(ctx, {
message: `This deployment is using an older version of the Convex backend. Upgrade now?`,
default: true,
}));
if (!confirmed) {
const { binaryPath: oldBinaryPath } = await ensureBackendBinaryDownloaded(
ctx,
{
kind: "version",
version: args.oldVersion,
},
);
// Skipping upgrade, save the config with the old version and run.
saveDeploymentConfig(ctx, args.deploymentName, {
...newConfig,
backendVersion: args.oldVersion,
});
return runLocalBackend(ctx, {
binaryPath: oldBinaryPath,
ports: args.ports,
deploymentName: args.deploymentName,
});
}
const choice = args.forceUpgrade
? "transfer"
: await promptOptions(ctx, {
message: "Transfer data from existing deployment?",
default: "transfer",
choices: [
{ name: "transfer data", value: "transfer" },
{ name: "start fresh", value: "reset" },
],
});
const deploymentStatePath = deploymentStateDir(args.deploymentName);
if (choice === "reset") {
recursivelyDelete(ctx, deploymentStatePath, { force: true });
saveDeploymentConfig(ctx, args.deploymentName, newConfig);
return runLocalBackend(ctx, {
binaryPath: args.newBinaryPath,
deploymentName: args.deploymentName,
ports: args.ports,
});
}
return handleUpgrade(ctx, {
deploymentName: args.deploymentName,
oldVersion: args.oldVersion,
newBinaryPath: args.newBinaryPath,
newVersion: args.newVersion,
ports: args.ports,
adminKey: args.adminKey,
});
}
async function handleUpgrade(
ctx: Context,
args: {
deploymentName: string;
oldVersion: string;
newBinaryPath: string;
newVersion: string;
ports: {
cloud: number;
site: number;
};
adminKey: string;
},
): Promise<{ cleanupHandle: () => Promise<void> }> {
const { binaryPath: oldBinaryPath } = await ensureBackendBinaryDownloaded(
ctx,
{
kind: "version",
version: args.oldVersion,
},
);
logVerbose(ctx, "Running backend on old version");
const { cleanupHandle: oldCleanupHandle } = await runLocalBackend(ctx, {
binaryPath: oldBinaryPath,
ports: args.ports,
deploymentName: args.deploymentName,
});
logVerbose(ctx, "Downloading env vars");
const deploymentUrl = localDeploymentUrl(args.ports.cloud);
const envs = (await runQuery(
ctx,
deploymentUrl,
args.adminKey,
"_system/cli/queryEnvironmentVariables",
{},
)) as Array<{
name: string;
value: string;
}>;
logVerbose(ctx, "Doing a snapshot export");
const exportPath = path.join(
deploymentStateDir(args.deploymentName),
"export.zip",
);
if (ctx.fs.exists(exportPath)) {
ctx.fs.unlink(exportPath);
}
const snaphsotExportState = await startSnapshotExport(ctx, {
deploymentName: args.deploymentName,
deploymentUrl,
adminKey: args.adminKey,
includeStorage: true,
inputPath: exportPath,
});
if (snaphsotExportState.state !== "completed") {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Failed to export snapshot",
});
}
await downloadSnapshotExport(ctx, {
snapshotExportTs: snaphsotExportState.complete_ts,
inputPath: exportPath,
adminKey: args.adminKey,
deploymentUrl,
});
logVerbose(ctx, "Stopping the backend on the old version");
await oldCleanupHandle();
await ensureBackendStopped(ctx, { ports: args.ports, maxTimeSecs: 5 });
// TODO(ENG-7078) save old artifacts to backup files
logVerbose(ctx, "Running backend on new version");
const { cleanupHandle } = await runLocalBackend(ctx, {
binaryPath: args.newBinaryPath,
ports: args.ports,
deploymentName: args.deploymentName,
});
logVerbose(ctx, "Importing the env vars");
if (envs.length > 0) {
const fetch = deploymentFetch(deploymentUrl, args.adminKey);
try {
await fetch("/api/update_environment_variables", {
body: JSON.stringify({ changes: envs }),
method: "POST",
});
} catch (e) {
return await logAndHandleFetchError(ctx, e);
}
}
logVerbose(ctx, "Doing a snapshot import");
const importId = await uploadForImport(ctx, {
deploymentUrl,
adminKey: args.adminKey,
filePath: exportPath,
importArgs: { format: "zip", mode: "replace", tableName: undefined },
onImportFailed: async (e) => {
logFailure(ctx, `Failed to import snapshot: ${e}`);
},
});
logVerbose(ctx, `Snapshot import started`);
let status = await waitForStableImportState(ctx, {
importId,
deploymentUrl,
adminKey: args.adminKey,
onProgress: () => {
// do nothing for now
return 0;
},
});
if (status.state !== "waiting_for_confirmation") {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Failed to upload snapshot",
});
}
await confirmImport(ctx, {
importId,
adminKey: args.adminKey,
deploymentUrl,
onError: async (e) => {
logFailure(ctx, `Failed to confirm import: ${e}`);
},
});
logVerbose(ctx, `Snapshot import confirmed`);
status = await waitForStableImportState(ctx, {
importId,
deploymentUrl,
adminKey: args.adminKey,
onProgress: () => {
// do nothing for now
return 0;
},
});
logVerbose(ctx, `Snapshot import status: ${status.state}`);
if (status.state !== "completed") {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: "Failed to import snapshot",
});
}
logFinishedStep(ctx, "Successfully upgraded to a new backend version");
saveDeploymentConfig(ctx, args.deploymentName, {
ports: args.ports,
backendVersion: args.newVersion,
adminKey: args.adminKey,
});
return { cleanupHandle };
}