chore: Fix typing in ctl backup and restore (#37663)
Now that we've moved to TypeScript, this PR fixes missing type annotations in `backup.ts` and `restore.ts`. ## Automation /test sanity ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/11988138792> > Commit: 5770dd0c1fe2c97787a6d8d454b36005d2f169f3 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11988138792&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Sun, 24 Nov 2024 01:02:33 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced error handling for password mismatches during backup encryption. - Improved test coverage for backup functionality, including backup paths and disk space validation. - **Bug Fixes** - Refined backup cleanup logic to ensure proper file retention and removal. - **Refactor** - Introduced a new `BackupState` class to manage backup process state. - Streamlined password handling and improved code structure for better readability. - Updated type safety for several functions in the backup and restore processes. - Simplified exit process in error handling for restore functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
7c66c07805
commit
392524e121
|
|
@ -230,9 +230,8 @@ describe("Backup Tests", () => {
|
|||
|
||||
return password;
|
||||
});
|
||||
const password_res = backup.getEncryptionPasswordFromUser();
|
||||
|
||||
expect(password_res).toEqual(-1);
|
||||
expect(() => backup.getEncryptionPasswordFromUser()).toThrow();
|
||||
});
|
||||
|
||||
test("Get encrypted archive path", async () => {
|
||||
|
|
|
|||
|
|
@ -10,26 +10,49 @@ import readlineSync from "readline-sync";
|
|||
|
||||
const command_args = process.argv.slice(3);
|
||||
|
||||
export async function run() {
|
||||
const timestamp = getTimeStampInISO();
|
||||
let errorCode = 0;
|
||||
let backupRootPath, archivePath, encryptionPassword;
|
||||
let encryptArchive = false;
|
||||
class BackupState {
|
||||
readonly initAt: string = getTimeStampInISO();
|
||||
readonly errors: string[] = [];
|
||||
|
||||
backupRootPath: string = "";
|
||||
archivePath: string = "";
|
||||
|
||||
encryptionPassword: string = "";
|
||||
|
||||
isEncryptionEnabled() {
|
||||
return !!this.encryptionPassword;
|
||||
}
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
await utils.ensureSupervisorIsRunning();
|
||||
|
||||
const state: BackupState = new BackupState();
|
||||
|
||||
try {
|
||||
// PRE-BACKUP
|
||||
console.log("Available free space at /appsmith-stacks");
|
||||
const availSpaceInBytes =
|
||||
getAvailableBackupSpaceInBytes("/appsmith-stacks");
|
||||
const availSpaceInBytes: number =
|
||||
await getAvailableBackupSpaceInBytes("/appsmith-stacks");
|
||||
|
||||
console.log("\n");
|
||||
|
||||
checkAvailableBackupSpace(availSpaceInBytes);
|
||||
|
||||
backupRootPath = await generateBackupRootPath();
|
||||
const backupContentsPath = getBackupContentsPath(backupRootPath, timestamp);
|
||||
if (
|
||||
!command_args.includes("--non-interactive") &&
|
||||
tty.isatty((process.stdout as any).fd)
|
||||
) {
|
||||
state.encryptionPassword = getEncryptionPasswordFromUser();
|
||||
}
|
||||
|
||||
state.backupRootPath = await generateBackupRootPath();
|
||||
const backupContentsPath: string = getBackupContentsPath(
|
||||
state.backupRootPath,
|
||||
state.initAt,
|
||||
);
|
||||
|
||||
// BACKUP
|
||||
await fsPromises.mkdir(backupContentsPath);
|
||||
|
||||
await exportDatabase(backupContentsPath);
|
||||
|
|
@ -38,30 +61,18 @@ export async function run() {
|
|||
|
||||
await createManifestFile(backupContentsPath);
|
||||
|
||||
if (
|
||||
!command_args.includes("--non-interactive") &&
|
||||
tty.isatty((process.stdout as any).fd)
|
||||
) {
|
||||
encryptionPassword = getEncryptionPasswordFromUser();
|
||||
await exportDockerEnvFile(backupContentsPath, state.isEncryptionEnabled());
|
||||
|
||||
if (encryptionPassword == -1) {
|
||||
throw new Error(
|
||||
"Backup process aborted because a valid enctyption password could not be obtained from the user",
|
||||
);
|
||||
}
|
||||
state.archivePath = await createFinalArchive(
|
||||
state.backupRootPath,
|
||||
state.initAt,
|
||||
);
|
||||
|
||||
encryptArchive = true;
|
||||
}
|
||||
|
||||
await exportDockerEnvFile(backupContentsPath, encryptArchive);
|
||||
|
||||
archivePath = await createFinalArchive(backupRootPath, timestamp);
|
||||
|
||||
// shell.exec("openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in " + archivePath + " -out " + archivePath + ".enc");
|
||||
if (encryptArchive) {
|
||||
// POST-BACKUP
|
||||
if (state.isEncryptionEnabled()) {
|
||||
const encryptedArchivePath = await encryptBackupArchive(
|
||||
archivePath,
|
||||
encryptionPassword,
|
||||
state.archivePath,
|
||||
state.encryptionPassword,
|
||||
);
|
||||
|
||||
await logger.backup_info(
|
||||
|
|
@ -69,12 +80,15 @@ export async function run() {
|
|||
encryptedArchivePath,
|
||||
);
|
||||
|
||||
if (archivePath != null) {
|
||||
await fsPromises.rm(archivePath, { recursive: true, force: true });
|
||||
if (state.archivePath != null) {
|
||||
await fsPromises.rm(state.archivePath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await logger.backup_info(
|
||||
"Finished creating a backup archive at " + archivePath,
|
||||
"Finished creating a backup archive at " + state.archivePath,
|
||||
);
|
||||
console.log(
|
||||
"********************************************************* IMPORTANT!!! *************************************************************",
|
||||
|
|
@ -90,11 +104,13 @@ export async function run() {
|
|||
);
|
||||
}
|
||||
|
||||
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
|
||||
await fsPromises.rm(state.backupRootPath, { recursive: true, force: true });
|
||||
|
||||
await logger.backup_info("Finished taking a backup at " + archivePath);
|
||||
await logger.backup_info(
|
||||
"Finished taking a backup at " + state.archivePath,
|
||||
);
|
||||
} catch (err) {
|
||||
errorCode = 1;
|
||||
process.exitCode = 1;
|
||||
await logger.backup_error(err.stack);
|
||||
|
||||
if (command_args.includes("--error-mail")) {
|
||||
|
|
@ -106,27 +122,36 @@ export async function run() {
|
|||
Constants.DURATION_BETWEEN_BACKUP_ERROR_MAILS_IN_MILLI_SEC <
|
||||
currentTS
|
||||
) {
|
||||
await mailer.sendBackupErrorToAdmins(err, timestamp);
|
||||
await mailer.sendBackupErrorToAdmins(err, state.initAt);
|
||||
await utils.updateLastBackupErrorMailSentInMilliSec(currentTS);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (backupRootPath != null) {
|
||||
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
|
||||
if (state.backupRootPath != null) {
|
||||
await fsPromises.rm(state.backupRootPath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (encryptArchive) {
|
||||
if (archivePath != null) {
|
||||
await fsPromises.rm(archivePath, { recursive: true, force: true });
|
||||
if (state.isEncryptionEnabled()) {
|
||||
if (state.archivePath != null) {
|
||||
await fsPromises.rm(state.archivePath, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await postBackupCleanup();
|
||||
process.exit(errorCode);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptBackupArchive(archivePath, encryptionPassword) {
|
||||
export async function encryptBackupArchive(
|
||||
archivePath: string,
|
||||
encryptionPassword: string,
|
||||
) {
|
||||
const encryptedArchivePath = archivePath + ".enc";
|
||||
|
||||
await utils.execCommand([
|
||||
|
|
@ -135,7 +160,7 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) {
|
|||
"-aes-256-cbc",
|
||||
"-pbkdf2",
|
||||
"-iter",
|
||||
100000,
|
||||
"100000",
|
||||
"-in",
|
||||
archivePath,
|
||||
"-out",
|
||||
|
|
@ -147,17 +172,17 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) {
|
|||
return encryptedArchivePath;
|
||||
}
|
||||
|
||||
export function getEncryptionPasswordFromUser() {
|
||||
export function getEncryptionPasswordFromUser(): string {
|
||||
for (const attempt of [1, 2, 3]) {
|
||||
if (attempt > 1) {
|
||||
console.log("Retry attempt", attempt);
|
||||
}
|
||||
|
||||
const encryptionPwd1 = readlineSync.question(
|
||||
const encryptionPwd1: string = readlineSync.question(
|
||||
"Enter a password to encrypt the backup archive: ",
|
||||
{ hideEchoBack: true },
|
||||
);
|
||||
const encryptionPwd2 = readlineSync.question(
|
||||
const encryptionPwd2: string = readlineSync.question(
|
||||
"Enter the above password again: ",
|
||||
{ hideEchoBack: true },
|
||||
);
|
||||
|
|
@ -179,16 +204,18 @@ export function getEncryptionPasswordFromUser() {
|
|||
"Aborting backup process, failed to obtain valid encryption password.",
|
||||
);
|
||||
|
||||
return -1;
|
||||
throw new Error(
|
||||
"Backup process aborted because a valid encryption password could not be obtained from the user",
|
||||
);
|
||||
}
|
||||
|
||||
async function exportDatabase(destFolder) {
|
||||
async function exportDatabase(destFolder: string) {
|
||||
console.log("Exporting database");
|
||||
await executeMongoDumpCMD(destFolder, utils.getDburl());
|
||||
console.log("Exporting database done.");
|
||||
}
|
||||
|
||||
async function createGitStorageArchive(destFolder) {
|
||||
async function createGitStorageArchive(destFolder: string) {
|
||||
console.log("Creating git-storage archive");
|
||||
|
||||
const gitRoot = getGitRoot(process.env.APPSMITH_GIT_ROOT);
|
||||
|
|
@ -198,7 +225,7 @@ async function createGitStorageArchive(destFolder) {
|
|||
console.log("Created git-storage archive");
|
||||
}
|
||||
|
||||
async function createManifestFile(path) {
|
||||
async function createManifestFile(path: string) {
|
||||
const version = await utils.getCurrentAppsmithVersion();
|
||||
const manifest_data = {
|
||||
appsmithVersion: version,
|
||||
|
|
@ -211,7 +238,10 @@ async function createManifestFile(path) {
|
|||
);
|
||||
}
|
||||
|
||||
async function exportDockerEnvFile(destFolder, encryptArchive) {
|
||||
async function exportDockerEnvFile(
|
||||
destFolder: string,
|
||||
encryptArchive: boolean,
|
||||
) {
|
||||
console.log("Exporting docker environment file");
|
||||
const content = await fsPromises.readFile(
|
||||
"/appsmith-stacks/configuration/docker.env",
|
||||
|
|
@ -231,7 +261,10 @@ async function exportDockerEnvFile(destFolder, encryptArchive) {
|
|||
console.log("Exporting docker environment file done.");
|
||||
}
|
||||
|
||||
export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
|
||||
export async function executeMongoDumpCMD(
|
||||
destFolder: string,
|
||||
appsmithMongoURI: string,
|
||||
) {
|
||||
return await utils.execCommand([
|
||||
"mongodump",
|
||||
`--uri=${appsmithMongoURI}`,
|
||||
|
|
@ -240,7 +273,7 @@ export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
|
|||
]); // generate cmd
|
||||
}
|
||||
|
||||
async function createFinalArchive(destFolder, timestamp) {
|
||||
async function createFinalArchive(destFolder: string, timestamp: string) {
|
||||
console.log("Creating final archive");
|
||||
|
||||
const archive = `${Constants.BACKUP_PATH}/appsmith-backup-${timestamp}.tar.gz`;
|
||||
|
|
@ -263,7 +296,7 @@ async function createFinalArchive(destFolder, timestamp) {
|
|||
async function postBackupCleanup() {
|
||||
console.log("Starting the cleanup task after taking a backup.");
|
||||
const backupArchivesLimit = getBackupArchiveLimit(
|
||||
process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT,
|
||||
parseInt(process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT, 10),
|
||||
);
|
||||
const backupFiles = await utils.listLocalBackupFiles();
|
||||
|
||||
|
|
@ -276,16 +309,16 @@ async function postBackupCleanup() {
|
|||
console.log("Cleanup task completed.");
|
||||
}
|
||||
|
||||
export async function executeCopyCMD(srcFolder, destFolder) {
|
||||
export async function executeCopyCMD(srcFolder: string, destFolder: string) {
|
||||
return await utils.execCommand([
|
||||
"ln",
|
||||
"-s",
|
||||
srcFolder,
|
||||
destFolder + "/git-storage",
|
||||
path.join(destFolder, "git-storage"),
|
||||
]);
|
||||
}
|
||||
|
||||
export function getGitRoot(gitRoot?) {
|
||||
export function getGitRoot(gitRoot?: string | undefined) {
|
||||
if (gitRoot == null || gitRoot === "") {
|
||||
gitRoot = "/appsmith-stacks/git-storage";
|
||||
}
|
||||
|
|
@ -297,11 +330,14 @@ export async function generateBackupRootPath() {
|
|||
return fsPromises.mkdtemp(path.join(os.tmpdir(), "appsmithctl-backup-"));
|
||||
}
|
||||
|
||||
export function getBackupContentsPath(backupRootPath, timestamp) {
|
||||
export function getBackupContentsPath(
|
||||
backupRootPath: string,
|
||||
timestamp: string,
|
||||
): string {
|
||||
return backupRootPath + "/appsmith-backup-" + timestamp;
|
||||
}
|
||||
|
||||
export function removeSensitiveEnvData(content) {
|
||||
export function removeSensitiveEnvData(content: string): string {
|
||||
// Remove encryption and Mongodb data from docker.env
|
||||
const output_lines = [];
|
||||
|
||||
|
|
@ -318,14 +354,14 @@ export function removeSensitiveEnvData(content) {
|
|||
return output_lines.join("\n");
|
||||
}
|
||||
|
||||
export function getBackupArchiveLimit(backupArchivesLimit?) {
|
||||
if (!backupArchivesLimit)
|
||||
backupArchivesLimit = Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT;
|
||||
|
||||
return backupArchivesLimit;
|
||||
export function getBackupArchiveLimit(backupArchivesLimit?: number): number {
|
||||
return backupArchivesLimit || Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT;
|
||||
}
|
||||
|
||||
export async function removeOldBackups(backupFiles, backupArchivesLimit) {
|
||||
export async function removeOldBackups(
|
||||
backupFiles: string[],
|
||||
backupArchivesLimit: number,
|
||||
) {
|
||||
while (backupFiles.length > backupArchivesLimit) {
|
||||
const fileName = backupFiles.shift();
|
||||
|
||||
|
|
@ -339,13 +375,15 @@ export function getTimeStampInISO() {
|
|||
return new Date().toISOString().replace(/:/g, "-");
|
||||
}
|
||||
|
||||
export async function getAvailableBackupSpaceInBytes(path) {
|
||||
export async function getAvailableBackupSpaceInBytes(
|
||||
path: string,
|
||||
): Promise<number> {
|
||||
const stat = await fsPromises.statfs(path);
|
||||
|
||||
return stat.bsize * stat.bfree;
|
||||
}
|
||||
|
||||
export function checkAvailableBackupSpace(availSpaceInBytes) {
|
||||
export function checkAvailableBackupSpace(availSpaceInBytes: number) {
|
||||
if (availSpaceInBytes < Constants.MIN_REQUIRED_DISK_SPACE_IN_BYTES) {
|
||||
throw new Error(
|
||||
"Not enough space available at /appsmith-stacks. Please ensure availability of at least 2GB to backup successfully.",
|
||||
|
|
|
|||
|
|
@ -57,7 +57,10 @@ async function getBackupFileName() {
|
|||
}
|
||||
}
|
||||
|
||||
async function decryptArchive(encryptedFilePath, backupFilePath) {
|
||||
async function decryptArchive(
|
||||
encryptedFilePath: string,
|
||||
backupFilePath: string,
|
||||
) {
|
||||
console.log("Enter the password to decrypt the backup archive:");
|
||||
|
||||
for (const attempt of [1, 2, 3]) {
|
||||
|
|
@ -75,7 +78,7 @@ async function decryptArchive(encryptedFilePath, backupFilePath) {
|
|||
"-aes-256-cbc",
|
||||
"-pbkdf2",
|
||||
"-iter",
|
||||
100000,
|
||||
"100000",
|
||||
"-in",
|
||||
encryptedFilePath,
|
||||
"-out",
|
||||
|
|
@ -93,7 +96,7 @@ async function decryptArchive(encryptedFilePath, backupFilePath) {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function extractArchive(backupFilePath, restoreRootPath) {
|
||||
async function extractArchive(backupFilePath: string, restoreRootPath: string) {
|
||||
console.log("Extracting the Appsmith backup archive at " + backupFilePath);
|
||||
await utils.execCommand([
|
||||
"tar",
|
||||
|
|
@ -105,7 +108,7 @@ async function extractArchive(backupFilePath, restoreRootPath) {
|
|||
console.log("Extracting the backup archive completed");
|
||||
}
|
||||
|
||||
async function restoreDatabase(restoreContentsPath, dbUrl) {
|
||||
async function restoreDatabase(restoreContentsPath: string, dbUrl: string) {
|
||||
console.log("Restoring database...");
|
||||
const cmd = [
|
||||
"mongorestore",
|
||||
|
|
@ -136,9 +139,9 @@ async function restoreDatabase(restoreContentsPath, dbUrl) {
|
|||
}
|
||||
|
||||
async function restoreDockerEnvFile(
|
||||
restoreContentsPath,
|
||||
backupName,
|
||||
overwriteEncryptionKeys,
|
||||
restoreContentsPath: string,
|
||||
backupName: string,
|
||||
overwriteEncryptionKeys: boolean,
|
||||
) {
|
||||
console.log("Restoring docker environment file");
|
||||
const dockerEnvFile = "/appsmith-stacks/configuration/docker.env";
|
||||
|
|
@ -227,9 +230,11 @@ async function restoreDockerEnvFile(
|
|||
console.log("Restoring docker environment file completed");
|
||||
}
|
||||
|
||||
async function restoreGitStorageArchive(restoreContentsPath, backupName) {
|
||||
async function restoreGitStorageArchive(
|
||||
restoreContentsPath: string,
|
||||
backupName: string,
|
||||
) {
|
||||
console.log("Restoring git-storage archive");
|
||||
// TODO: Consider APPSMITH_GIT_ROOT env for later iterations
|
||||
const gitRoot = "/appsmith-stacks/git-storage";
|
||||
|
||||
await utils.execCommand(["mv", gitRoot, gitRoot + "-" + backupName]);
|
||||
|
|
@ -241,7 +246,7 @@ async function restoreGitStorageArchive(restoreContentsPath, backupName) {
|
|||
console.log("Restoring git-storage archive completed");
|
||||
}
|
||||
|
||||
async function checkRestoreVersionCompatability(restoreContentsPath) {
|
||||
async function checkRestoreVersionCompatability(restoreContentsPath: string) {
|
||||
const currentVersion = await utils.getCurrentAppsmithVersion();
|
||||
const manifest_data = await fsPromises.readFile(
|
||||
restoreContentsPath + "/manifest.json",
|
||||
|
|
@ -280,7 +285,7 @@ async function checkRestoreVersionCompatability(restoreContentsPath) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getBackupDatabaseName(restoreContentsPath) {
|
||||
async function getBackupDatabaseName(restoreContentsPath: string) {
|
||||
let db_name = "appsmith";
|
||||
|
||||
if (command_args.includes("--backup-db-name")) {
|
||||
|
|
@ -307,10 +312,9 @@ async function getBackupDatabaseName(restoreContentsPath) {
|
|||
}
|
||||
|
||||
export async function run() {
|
||||
let errorCode = 0;
|
||||
let cleanupArchive = false;
|
||||
let overwriteEncryptionKeys = true;
|
||||
let backupFilePath;
|
||||
let backupFilePath: string;
|
||||
|
||||
await utils.ensureSupervisorIsRunning();
|
||||
|
||||
|
|
@ -318,7 +322,7 @@ export async function run() {
|
|||
let backupFileName = await getBackupFileName();
|
||||
|
||||
if (backupFileName == null) {
|
||||
process.exit(errorCode);
|
||||
process.exit();
|
||||
} else {
|
||||
backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName);
|
||||
|
||||
|
|
@ -342,7 +346,7 @@ export async function run() {
|
|||
"You have entered the incorrect password multiple times. Aborting the restore process.",
|
||||
);
|
||||
await fsPromises.rm(backupFilePath, { force: true });
|
||||
process.exit(errorCode);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,17 +376,17 @@ export async function run() {
|
|||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
errorCode = 1;
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (cleanupArchive) {
|
||||
await fsPromises.rm(backupFilePath, { force: true });
|
||||
}
|
||||
|
||||
await utils.start(["backend", "rts"]);
|
||||
process.exit(errorCode);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
function isArchiveEncrypted(backupFilePath) {
|
||||
function isArchiveEncrypted(backupFilePath: string) {
|
||||
return backupFilePath.endsWith(".enc");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user