feat: Support encrypted backups and fix restoring to renamed databases (#29902)
Fixes: [31004](https://github.com/appsmithorg/appsmith/issues/31004) Co-authored-by: Shrikant Sharat Kandula <shrikant@appsmith.com>
This commit is contained in:
parent
92ff6a9c7d
commit
f85d64d775
|
|
@ -6,12 +6,16 @@ const utils = require('./utils');
|
||||||
const Constants = require('./constants');
|
const Constants = require('./constants');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const mailer = require('./mailer');
|
const mailer = require('./mailer');
|
||||||
|
const tty = require('tty');
|
||||||
|
const readlineSync = require('readline-sync');
|
||||||
|
|
||||||
const command_args = process.argv.slice(3);
|
const command_args = process.argv.slice(3);
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const timestamp = getTimeStampInISO();
|
const timestamp = getTimeStampInISO();
|
||||||
let errorCode = 0;
|
let errorCode = 0;
|
||||||
|
let backupRootPath, archivePath, encryptionPassword;
|
||||||
|
let encryptArchive = false;
|
||||||
try {
|
try {
|
||||||
const check_supervisord_status_cmd = '/usr/bin/supervisorctl >/dev/null 2>&1';
|
const check_supervisord_status_cmd = '/usr/bin/supervisorctl >/dev/null 2>&1';
|
||||||
shell.exec(check_supervisord_status_cmd, function (code) {
|
shell.exec(check_supervisord_status_cmd, function (code) {
|
||||||
|
|
@ -37,13 +41,36 @@ async function run() {
|
||||||
await createGitStorageArchive(backupContentsPath);
|
await createGitStorageArchive(backupContentsPath);
|
||||||
|
|
||||||
await createManifestFile(backupContentsPath);
|
await createManifestFile(backupContentsPath);
|
||||||
await exportDockerEnvFile(backupContentsPath);
|
|
||||||
|
if (!command_args.includes('--non-interactive') && (tty.isatty(process.stdout.fd))){
|
||||||
|
encryptionPassword = getEncryptionPasswordFromUser();
|
||||||
|
if (encryptionPassword == -1){
|
||||||
|
throw new Error('Backup process aborted because a valid enctyption password could not be obtained from the user');
|
||||||
|
}
|
||||||
|
encryptArchive = true;
|
||||||
|
}
|
||||||
|
await exportDockerEnvFile(backupContentsPath, encryptArchive);
|
||||||
|
|
||||||
const archivePath = await createFinalArchive(backupRootPath, timestamp);
|
const archivePath = await createFinalArchive(backupRootPath, timestamp);
|
||||||
|
// shell.exec("openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in " + archivePath + " -out " + archivePath + ".enc");
|
||||||
|
if (encryptArchive){
|
||||||
|
const encryptedArchivePath = await encryptBackupArchive(archivePath,encryptionPassword);
|
||||||
|
logger.backup_info('Finished creating an encrypted a backup archive at ' + encryptedArchivePath);
|
||||||
|
if (archivePath != null) {
|
||||||
|
await fsPromises.rm(archivePath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.backup_info('Finished creating a backup archive at ' + archivePath);
|
||||||
|
console.log('********************************************************* IMPORTANT!!! *************************************************************');
|
||||||
|
console.log('*** Please ensure you have saved the APPSMITH_ENCRYPTION_SALT and APPSMITH_ENCRYPTION_PASSWORD variables from the docker.env file **')
|
||||||
|
console.log('*** These values are not included in the backup export. **');
|
||||||
|
console.log('************************************************************************************************************************************');
|
||||||
|
}
|
||||||
|
|
||||||
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
|
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
|
||||||
|
|
||||||
logger.backup_info('Finished taking a backup at' + archivePath);
|
logger.backup_info('Finished taking a backup at ' + archivePath);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorCode = 1;
|
errorCode = 1;
|
||||||
|
|
@ -58,11 +85,44 @@ async function run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (backupRootPath != null) {
|
||||||
|
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
if (encryptArchive) {
|
||||||
|
if (archivePath != null) {
|
||||||
|
await fsPromises.rm(archivePath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
await postBackupCleanup();
|
await postBackupCleanup();
|
||||||
process.exit(errorCode);
|
process.exit(errorCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function encryptBackupArchive(archivePath, encryptionPassword){
|
||||||
|
const encryptedArchivePath = archivePath + '.enc';
|
||||||
|
await utils.execCommand(['openssl', 'enc', '-aes-256-cbc', '-pbkdf2', '-iter', 100000, '-in', archivePath, '-out', encryptedArchivePath, '-k', encryptionPassword ])
|
||||||
|
return encryptedArchivePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEncryptionPasswordFromUser(){
|
||||||
|
for (const _ of [1, 2, 3])
|
||||||
|
{
|
||||||
|
const encryptionPwd1 = readlineSync.question('Enter a password to encrypt the backup archive: ', { hideEchoBack: true });
|
||||||
|
const encryptionPwd2 = readlineSync.question('Enter the above password again: ', { hideEchoBack: true });
|
||||||
|
if (encryptionPwd1 === encryptionPwd2){
|
||||||
|
if (encryptionPwd1){
|
||||||
|
return encryptionPwd1;
|
||||||
|
}
|
||||||
|
console.error("Invalid input. Empty password is not allowed, please try again.")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error("The passwords do not match, please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("Aborting backup process, failed to obtain valid encryption password.");
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
async function exportDatabase(destFolder) {
|
async function exportDatabase(destFolder) {
|
||||||
console.log('Exporting database');
|
console.log('Exporting database');
|
||||||
await executeMongoDumpCMD(destFolder, process.env.APPSMITH_MONGODB_URI)
|
await executeMongoDumpCMD(destFolder, process.env.APPSMITH_MONGODB_URI)
|
||||||
|
|
@ -81,19 +141,20 @@ async function createGitStorageArchive(destFolder) {
|
||||||
|
|
||||||
async function createManifestFile(path) {
|
async function createManifestFile(path) {
|
||||||
const version = await utils.getCurrentAppsmithVersion()
|
const version = await utils.getCurrentAppsmithVersion()
|
||||||
const manifest_data = { "appsmithVersion": version }
|
const manifest_data = { "appsmithVersion": version, "dbName": utils.getDatabaseNameFromMongoURI(process.env.APPSMITH_MONGODB_URI) }
|
||||||
await fsPromises.writeFile(path + '/manifest.json', JSON.stringify(manifest_data));
|
await fsPromises.writeFile(path + '/manifest.json', JSON.stringify(manifest_data));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportDockerEnvFile(destFolder) {
|
async function exportDockerEnvFile(destFolder, encryptArchive) {
|
||||||
console.log('Exporting docker environment file');
|
console.log('Exporting docker environment file');
|
||||||
const content = await fsPromises.readFile('/appsmith-stacks/configuration/docker.env', { encoding: 'utf8' });
|
const content = await fsPromises.readFile('/appsmith-stacks/configuration/docker.env', { encoding: 'utf8' });
|
||||||
const cleaned_content = removeSensitiveEnvData(content)
|
let cleaned_content = removeSensitiveEnvData(content);
|
||||||
|
if (encryptArchive){
|
||||||
|
cleaned_content += '\nAPPSMITH_ENCRYPTION_SALT=' + process.env.APPSMITH_ENCRYPTION_SALT +
|
||||||
|
'\nAPPSMITH_ENCRYPTION_PASSWORD=' + process.env.APPSMITH_ENCRYPTION_PASSWORD
|
||||||
|
}
|
||||||
await fsPromises.writeFile(destFolder + '/docker.env', cleaned_content);
|
await fsPromises.writeFile(destFolder + '/docker.env', cleaned_content);
|
||||||
console.log('Exporting docker environment file done.');
|
console.log('Exporting docker environment file done.');
|
||||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!! Important !!!!!!!!!!!!!!!!!!!!!!!!!!');
|
|
||||||
console.log('!!! Please ensure you have saved the APPSMITH_ENCRYPTION_SALT and APPSMITH_ENCRYPTION_PASSWORD variables from the docker.env file because those values are not included in the backup export.');
|
|
||||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
|
async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
|
||||||
|
|
@ -195,5 +256,7 @@ module.exports = {
|
||||||
executeCopyCMD,
|
executeCopyCMD,
|
||||||
removeSensitiveEnvData,
|
removeSensitiveEnvData,
|
||||||
getBackupArchiveLimit,
|
getBackupArchiveLimit,
|
||||||
removeOldBackups
|
removeOldBackups,
|
||||||
|
getEncryptionPasswordFromUser,
|
||||||
|
encryptBackupArchive,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const os = require('os');
|
||||||
const fsPromises = require('fs/promises');
|
const fsPromises = require('fs/promises');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const shell = require('shelljs');
|
const shell = require('shelljs');
|
||||||
|
const readlineSync = require('readline-sync');
|
||||||
|
|
||||||
describe('Backup Tests', () => {
|
describe('Backup Tests', () => {
|
||||||
|
|
||||||
|
|
@ -78,16 +79,18 @@ test('Test ln command generation', async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Checks for the current Appsmith Version.', async () => {
|
it('Checks for the current Appsmith Version.', async () => {
|
||||||
|
fsPromises.readFile = jest.fn().mockImplementation(async (a) =>
|
||||||
|
`Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.VERSION = void 0;
|
||||||
|
exports.VERSION = "v0.0.0-SNAPSHOT";`);
|
||||||
|
const res = await utils.getCurrentAppsmithVersion()
|
||||||
|
expect(res).toBe("v0.0.0-SNAPSHOT")
|
||||||
|
console.log(res)
|
||||||
fsPromises.readFile = jest.fn().mockImplementation(async () => `{"githubRef":"refs/tags/v1.2.3"}`);
|
fsPromises.readFile = jest.fn().mockImplementation(async () => `{"githubRef":"refs/tags/v1.2.3"}`);
|
||||||
await expect(utils.getCurrentAppsmithVersion()).resolves.toBe("v1.2.3")
|
await expect(utils.getCurrentAppsmithVersion()).resolves.toBe("v1.2.3")
|
||||||
})
|
})
|
||||||
|
|
||||||
test('If Encryption env values are being removed', () => {
|
test('If MONGODB and Encryption env values are being removed', () => {
|
||||||
expect(backup.removeSensitiveEnvData(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_ENCRYPTION_PASSWORD=dummy-pass\nAPPSMITH_ENCRYPTION_SALT=dummy-salt\nAPPSMITH_INSTANCE_NAME=Appsmith\n
|
|
||||||
`)).toMatch(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_INSTANCE_NAME=Appsmith\n`)
|
|
||||||
});
|
|
||||||
|
|
||||||
test('If MONGODB env values are being removed', () => {
|
|
||||||
expect(backup.removeSensitiveEnvData(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_MONGODB_URI=mongodb://appsmith:pass@localhost:27017/appsmith\nAPPSMITH_MONGODB_USER=appsmith\nAPPSMITH_MONGODB_PASSWORD=pass\nAPPSMITH_INSTANCE_NAME=Appsmith\n
|
expect(backup.removeSensitiveEnvData(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_MONGODB_URI=mongodb://appsmith:pass@localhost:27017/appsmith\nAPPSMITH_MONGODB_USER=appsmith\nAPPSMITH_MONGODB_PASSWORD=pass\nAPPSMITH_INSTANCE_NAME=Appsmith\n
|
||||||
`)).toMatch(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_INSTANCE_NAME=Appsmith\n`)
|
`)).toMatch(`APPSMITH_REDIS_URL=redis://127.0.0.1:6379\nAPPSMITH_INSTANCE_NAME=Appsmith\n`)
|
||||||
});
|
});
|
||||||
|
|
@ -182,5 +185,72 @@ test('Cleanup Backups when limit is 2 and there is no file', async () => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
expect(res).toEqual(expectedBackupFiles)
|
expect(res).toEqual(expectedBackupFiles)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('Test get encryption password from user prompt whene both passords are the same', async () => {
|
||||||
|
const password = 'password#4321'
|
||||||
|
readlineSync.question = jest.fn().mockImplementation((a) => {return password});
|
||||||
|
const password_res = backup.getEncryptionPasswordFromUser()
|
||||||
|
|
||||||
|
expect(password_res).toEqual(password)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test get encryption password from user prompt when both passords are the different', async () => {
|
||||||
|
const password = 'password#4321'
|
||||||
|
readlineSync.question = jest.fn().mockImplementation((a) => {
|
||||||
|
if (a=='Enter the above password again: '){
|
||||||
|
return 'pass';
|
||||||
|
}
|
||||||
|
return password});
|
||||||
|
const password_res = backup.getEncryptionPasswordFromUser()
|
||||||
|
|
||||||
|
expect(password_res).toEqual(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Get encrypted archive path', async () => {
|
||||||
|
const archivePath = '/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z';
|
||||||
|
const encryptionPassword = 'password#4321'
|
||||||
|
utils.execCommand = jest.fn().mockImplementation( async (a) => console.log(a));
|
||||||
|
const encArchivePath = await backup.encryptBackupArchive(archivePath, encryptionPassword)
|
||||||
|
|
||||||
|
expect(encArchivePath).toEqual('/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z' + '.enc')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Test backup encryption function', async () => {
|
||||||
|
utils.execCommand= jest.fn().mockImplementation(async (a) => console.log(a));
|
||||||
|
const archivePath = '/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z'
|
||||||
|
const encryptionPassword = 'password#123'
|
||||||
|
const res = await backup.encryptBackupArchive(archivePath,encryptionPassword)
|
||||||
|
console.log(res)
|
||||||
|
expect(res).toEqual('/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z.enc')
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Get DB name from Mongo URI 1', async () => {
|
||||||
|
var mongodb_uri = "mongodb+srv://admin:password@test.cluster.mongodb.net/my_db_name?retryWrites=true&minPoolSize=1&maxPoolSize=10&maxIdleTimeMS=900000&authSource=admin"
|
||||||
|
var expectedDBName = 'my_db_name'
|
||||||
|
const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri)
|
||||||
|
expect(dbName).toEqual(expectedDBName)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Get DB name from Mongo URI 2', async () => {
|
||||||
|
var mongodb_uri = "mongodb+srv://admin:password@test.cluster.mongodb.net/test123?retryWrites=true&minPoolSize=1&maxPoolSize=10&maxIdleTimeMS=900000&authSource=admin"
|
||||||
|
var expectedDBName = 'test123'
|
||||||
|
const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri)
|
||||||
|
expect(dbName).toEqual(expectedDBName)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Get DB name from Mongo URI 3', async () => {
|
||||||
|
var mongodb_uri = "mongodb+srv://admin:password@test.cluster.mongodb.net/test123"
|
||||||
|
var expectedDBName = 'test123'
|
||||||
|
const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri)
|
||||||
|
expect(dbName).toEqual(expectedDBName)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Get DB name from Mongo URI 4', async () => {
|
||||||
|
var mongodb_uri = "mongodb://appsmith:pAssW0rd!@localhost:27017/appsmith"
|
||||||
|
var expectedDBName = 'appsmith'
|
||||||
|
const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri)
|
||||||
|
expect(dbName).toEqual(expectedDBName)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const shell = require('shelljs');
|
||||||
|
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const Constants = require('./constants');
|
const Constants = require('./constants');
|
||||||
|
const command_args = process.argv.slice(3);
|
||||||
const {getCurrentAppsmithVersion} = require("./utils")
|
const {getCurrentAppsmithVersion} = require("./utils")
|
||||||
|
|
||||||
async function getBackupFileName() {
|
async function getBackupFileName() {
|
||||||
|
|
@ -19,7 +20,7 @@ async function getBackupFileName() {
|
||||||
console.log('----------------------------------------------------------------');
|
console.log('----------------------------------------------------------------');
|
||||||
console.log('Index\t|\tAppsmith Backup Archive File');
|
console.log('Index\t|\tAppsmith Backup Archive File');
|
||||||
console.log('----------------------------------------------------------------');
|
console.log('----------------------------------------------------------------');
|
||||||
for (var i = 0; i < backupFiles.length; i++) {
|
for (let i = 0; i < backupFiles.length; i++) {
|
||||||
if (i === backupFiles.length - 1)
|
if (i === backupFiles.length - 1)
|
||||||
console.log(i + '\t|\t' + backupFiles[i] + ' <--Most recent backup');
|
console.log(i + '\t|\t' + backupFiles[i] + ' <--Most recent backup');
|
||||||
else
|
else
|
||||||
|
|
@ -27,15 +28,27 @@ async function getBackupFileName() {
|
||||||
}
|
}
|
||||||
console.log('----------------------------------------------------------------');
|
console.log('----------------------------------------------------------------');
|
||||||
|
|
||||||
var backupFileIndex = parseInt(readlineSync.question('Please enter the backup file index: '), 10);
|
const backupFileIndex = parseInt(readlineSync.question('Please enter the backup file index: '), 10);
|
||||||
if (!isNaN(backupFileIndex) && Number.isInteger(backupFileIndex) && (backupFileIndex >= 0) && (backupFileIndex < backupFiles.length)) {
|
if (!isNaN(backupFileIndex) && Number.isInteger(backupFileIndex) && (backupFileIndex >= 0) && (backupFileIndex < backupFiles.length)) {
|
||||||
return backupFiles[parseInt(backupFileIndex, 10)];
|
return backupFiles[parseInt(backupFileIndex, 10)];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log('Invalid input, please try the command again with a valid option');
|
console.log('Invalid input, please try the command again with a valid option');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptArchive(encryptedFilePath, backupFilePath){
|
||||||
|
console.log('Enter the password to decrypt the backup archive:')
|
||||||
|
for (const _ of [1, 2, 3]) {
|
||||||
|
const decryptionPwd = readlineSync.question('', { hideEchoBack: true });
|
||||||
|
try{
|
||||||
|
await utils.execCommandSilent(['openssl', 'enc', '-d', '-aes-256-cbc', '-pbkdf2', '-iter', 100000, '-in', encryptedFilePath, '-out', backupFilePath, '-k', decryptionPwd])
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Invalid password. Please try again:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractArchive(backupFilePath, restoreRootPath) {
|
async function extractArchive(backupFilePath, restoreRootPath) {
|
||||||
|
|
@ -45,25 +58,46 @@ async function extractArchive(backupFilePath, restoreRootPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreDatabase(restoreContentsPath) {
|
async function restoreDatabase(restoreContentsPath) {
|
||||||
console.log('Restoring database ....');
|
console.log('Restoring database...');
|
||||||
await utils.execCommand(['mongorestore', `--uri=${process.env.APPSMITH_MONGODB_URI}`, '--drop', `--archive=${restoreContentsPath}/mongodb-data.gz`, '--gzip']);
|
const cmd = ['mongorestore', `--uri=${process.env.APPSMITH_MONGODB_URI}`, '--drop', `--archive=${restoreContentsPath}/mongodb-data.gz`, '--gzip']
|
||||||
|
try {
|
||||||
|
const fromDbName = await getBackupDatabaseName(restoreContentsPath);
|
||||||
|
const toDbName = utils.getDatabaseNameFromMongoURI(process.env.APPSMITH_MONGODB_URI);
|
||||||
|
console.log("Restoring database from " + fromDbName + " to " + toDbName)
|
||||||
|
cmd.push('--nsInclude=*', `--nsFrom=${fromDbName}.*`, `--nsTo=${toDbName}.*`)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error reading manifest file. Assuming same database name.', error);
|
||||||
|
}
|
||||||
|
await utils.execCommand(cmd);
|
||||||
console.log('Restoring database completed');
|
console.log('Restoring database completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreDockerEnvFile(restoreContentsPath, backupName) {
|
async function restoreDockerEnvFile(restoreContentsPath, backupName, overwriteEncryptionKeys) {
|
||||||
console.log('Restoring docker environment file');
|
console.log('Restoring docker environment file');
|
||||||
const dockerEnvFile = '/appsmith-stacks/configuration/docker.env';
|
const dockerEnvFile = '/appsmith-stacks/configuration/docker.env';
|
||||||
var encryptionPwd = process.env.APPSMITH_ENCRYPTION_PASSWORD;
|
let encryptionPwd = process.env.APPSMITH_ENCRYPTION_PASSWORD;
|
||||||
var encryptionSalt = process.env.APPSMITH_ENCRYPTION_SALT;
|
let encryptionSalt = process.env.APPSMITH_ENCRYPTION_SALT;
|
||||||
await utils.execCommand(['mv', dockerEnvFile, dockerEnvFile + '.' + backupName]);
|
await utils.execCommand(['mv', dockerEnvFile, dockerEnvFile + '.' + backupName]);
|
||||||
await utils.execCommand(['cp', restoreContentsPath + '/docker.env', dockerEnvFile]);
|
await utils.execCommand(['cp', restoreContentsPath + '/docker.env', dockerEnvFile]);
|
||||||
|
if (overwriteEncryptionKeys){
|
||||||
if (encryptionPwd && encryptionSalt) {
|
if (encryptionPwd && encryptionSalt) {
|
||||||
const input = readlineSync.question('If you are restoring to the same Appsmith deployment which generated the backup archive, you can use the existing encryption keys on the instance.\n\
|
const input = readlineSync.question('If you are restoring to the same Appsmith deployment which generated the backup archive, you can use the existing encryption keys on the instance.\n\
|
||||||
Press Enter to continue with existing encryption keys\n\
|
Press Enter to continue with existing encryption keys\n\
|
||||||
Or Type "n"/"No" to provide encryption key & password corresponding to the original Appsmith instance that is being restored.\n');
|
Or Type "n"/"No" to provide encryption key & password corresponding to the original Appsmith instance that is being restored.\n');
|
||||||
const answer = input && input.toLocaleUpperCase();
|
const answer = input && input.toLocaleUpperCase();
|
||||||
if (answer === 'N' || answer === 'NO') {
|
if (answer === 'N' || answer === 'NO') {
|
||||||
|
encryptionPwd = readlineSync.question('Enter the APPSMITH_ENCRYPTION_PASSWORD: ', {
|
||||||
|
hideEchoBack: true
|
||||||
|
});
|
||||||
|
encryptionSalt = readlineSync.question('Enter the APPSMITH_ENCRYPTION_SALT: ', {
|
||||||
|
hideEchoBack: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Restoring docker environment file with existing encryption password & salt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
encryptionPwd = readlineSync.question('Enter the APPSMITH_ENCRYPTION_PASSWORD: ', {
|
encryptionPwd = readlineSync.question('Enter the APPSMITH_ENCRYPTION_PASSWORD: ', {
|
||||||
hideEchoBack: true
|
hideEchoBack: true
|
||||||
});
|
});
|
||||||
|
|
@ -71,24 +105,13 @@ async function restoreDockerEnvFile(restoreContentsPath, backupName) {
|
||||||
hideEchoBack: true
|
hideEchoBack: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
await fsPromises.appendFile(dockerEnvFile, '\nAPPSMITH_ENCRYPTION_PASSWORD=' + encryptionPwd + '\nAPPSMITH_ENCRYPTION_SALT=' + encryptionSalt + '\nAPPSMITH_MONGODB_URI=' + process.env.APPSMITH_MONGODB_URI +
|
||||||
console.log('Restoring docker environment file with existing encryption password & salt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
encryptionPwd = readlineSync.question('Enter the APPSMITH_ENCRYPTION_PASSWORD: ', {
|
|
||||||
hideEchoBack: true
|
|
||||||
});
|
|
||||||
encryptionSalt = readlineSync.question('Enter the APPSMITH_ENCRYPTION_SALT: ', {
|
|
||||||
hideEchoBack: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsPromises.appendFile(dockerEnvFile, '\nAPPSMITH_ENCRYPTION_PASSWORD=' + encryptionPwd +
|
|
||||||
'\nAPPSMITH_ENCRYPTION_SALT=' + encryptionSalt + '\nAPPSMITH_MONGODB_URI=' + process.env.APPSMITH_MONGODB_URI +
|
|
||||||
'\nAPPSMITH_MONGODB_USER=' + process.env.APPSMITH_MONGODB_USER + '\nAPPSMITH_MONGODB_PASSWORD=' + process.env.APPSMITH_MONGODB_PASSWORD ) ;
|
'\nAPPSMITH_MONGODB_USER=' + process.env.APPSMITH_MONGODB_USER + '\nAPPSMITH_MONGODB_PASSWORD=' + process.env.APPSMITH_MONGODB_PASSWORD ) ;
|
||||||
|
} else {
|
||||||
console.log('Restoring docker environment file completed');
|
await fsPromises.appendFile(dockerEnvFile, '\nAPPSMITH_MONGODB_URI=' + process.env.APPSMITH_MONGODB_URI +
|
||||||
|
'\nAPPSMITH_MONGODB_USER=' + process.env.APPSMITH_MONGODB_USER + '\nAPPSMITH_MONGODB_PASSWORD=' + process.env.APPSMITH_MONGODB_PASSWORD ) ;
|
||||||
|
}
|
||||||
|
console.log('Restoring docker environment file completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreGitStorageArchive(restoreContentsPath, backupName) {
|
async function restoreGitStorageArchive(restoreContentsPath, backupName) {
|
||||||
|
|
@ -124,22 +147,58 @@ async function checkRestoreVersionCompatability(restoreContentsPath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getBackupDatabaseName(restoreContentsPath) {
|
||||||
|
let db_name = "appsmith"
|
||||||
|
if (command_args.includes('--backup-db-name')) {
|
||||||
|
for (let i = 0; i < command_args.length; i++) {
|
||||||
|
if (command_args[i].startsWith('--backup-db-name')) {
|
||||||
|
db_name = command_args[i].split("=")[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const manifest_data = await fsPromises.readFile(restoreContentsPath + '/manifest.json', { encoding: 'utf8' });
|
||||||
|
const manifest_json = JSON.parse(manifest_data);
|
||||||
|
if ("dbName" in manifest_json){
|
||||||
|
db_name = manifest_json["dbName"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Backup Database Name: ' + db_name);
|
||||||
|
return db_name
|
||||||
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
let errorCode = 0;
|
let errorCode = 0;
|
||||||
|
let cleanupArchive = false;
|
||||||
|
let overwriteEncryptionKeys = true;
|
||||||
|
let backupFilePath;
|
||||||
try {
|
try {
|
||||||
check_supervisord_status_cmd = '/usr/bin/supervisorctl >/dev/null 2>&1';
|
shell.exec('/usr/bin/supervisorctl >/dev/null 2>&1', function (code) {
|
||||||
shell.exec(check_supervisord_status_cmd, function (code) {
|
|
||||||
if (code > 0) {
|
if (code > 0) {
|
||||||
shell.echo('application is not running, starting supervisord');
|
shell.echo('application is not running, starting supervisord');
|
||||||
shell.exec('/usr/bin/supervisord');
|
shell.exec('/usr/bin/supervisord');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const backupFileName = await getBackupFileName();
|
let backupFileName = await getBackupFileName();
|
||||||
if (backupFileName == null) {
|
if (backupFileName == null) {
|
||||||
process.exit(errorCode);
|
process.exit(errorCode);
|
||||||
} else {
|
} else {
|
||||||
const backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName);
|
backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName);
|
||||||
|
if (isArchiveEncrypted(backupFileName)){
|
||||||
|
const encryptedBackupFilePath = path.join(Constants.BACKUP_PATH, backupFileName);
|
||||||
|
backupFileName = backupFileName.replace('.enc', '');
|
||||||
|
backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName);
|
||||||
|
cleanupArchive = true;
|
||||||
|
overwriteEncryptionKeys = false;
|
||||||
|
const decryptSuccess = await decryptArchive(encryptedBackupFilePath, backupFilePath);
|
||||||
|
if (!decryptSuccess){
|
||||||
|
console.log('You have entered the incorrect password multiple times. Aborting the restore process.')
|
||||||
|
await fsPromises.rm(backupFilePath, { force: true });
|
||||||
|
process.exit(errorCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
const backupName = backupFileName.replace(/\.tar\.gz$/, "");
|
const backupName = backupFileName.replace(/\.tar\.gz$/, "");
|
||||||
const restoreRootPath = await fsPromises.mkdtemp(os.tmpdir());
|
const restoreRootPath = await fsPromises.mkdtemp(os.tmpdir());
|
||||||
const restoreContentsPath = path.join(restoreRootPath, backupName);
|
const restoreContentsPath = path.join(restoreRootPath, backupName);
|
||||||
|
|
@ -151,7 +210,7 @@ async function run() {
|
||||||
console.log('Restoring Appsmith instance from the backup at ' + backupFilePath);
|
console.log('Restoring Appsmith instance from the backup at ' + backupFilePath);
|
||||||
utils.stop(['backend', 'rts']);
|
utils.stop(['backend', 'rts']);
|
||||||
await restoreDatabase(restoreContentsPath);
|
await restoreDatabase(restoreContentsPath);
|
||||||
await restoreDockerEnvFile(restoreContentsPath, backupName);
|
await restoreDockerEnvFile(restoreContentsPath, backupName, overwriteEncryptionKeys);
|
||||||
await restoreGitStorageArchive(restoreContentsPath, backupName);
|
await restoreGitStorageArchive(restoreContentsPath, backupName);
|
||||||
console.log('Appsmith instance successfully restored.');
|
console.log('Appsmith instance successfully restored.');
|
||||||
await fsPromises.rm(restoreRootPath, { recursive: true, force: true });
|
await fsPromises.rm(restoreRootPath, { recursive: true, force: true });
|
||||||
|
|
@ -161,12 +220,18 @@ async function run() {
|
||||||
errorCode = 1;
|
errorCode = 1;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
if (cleanupArchive){
|
||||||
|
await fsPromises.rm(backupFilePath, { force: true });
|
||||||
|
}
|
||||||
utils.start(['backend', 'rts']);
|
utils.start(['backend', 'rts']);
|
||||||
process.exit(errorCode);
|
process.exit(errorCode);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isArchiveEncrypted(backupFilePath){
|
||||||
|
return backupFilePath.endsWith('.enc');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
run,
|
run,
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ async function listLocalBackupFiles() {
|
||||||
.readdir(Constants.BACKUP_PATH)
|
.readdir(Constants.BACKUP_PATH)
|
||||||
.then((filenames) => {
|
.then((filenames) => {
|
||||||
for (let filename of filenames) {
|
for (let filename of filenames) {
|
||||||
if (filename.match(/^appsmith-backup-.*\.tar\.gz$/)) {
|
if (filename.match(/^appsmith-backup-.*\.tar\.gz(\.enc)?$/)) {
|
||||||
backupFiles.push(filename);
|
backupFiles.push(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +129,41 @@ function preprocessMongoDBURI(uri /* string */) {
|
||||||
|
|
||||||
return cs.toString();
|
return cs.toString();
|
||||||
}
|
}
|
||||||
|
function execCommandSilent(cmd, options) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let isPromiseDone = false;
|
||||||
|
|
||||||
|
const p = childProcess.spawn(cmd[0], cmd.slice(1), {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
p.on("exit", (code) => {
|
||||||
|
if (isPromiseDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isPromiseDone = true;
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
p.on("error", (err) => {
|
||||||
|
if (isPromiseDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isPromiseDone = true;
|
||||||
|
console.error("Error running command", err);
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDatabaseNameFromMongoURI(uri) {
|
||||||
|
const uriParts = uri.split("/");
|
||||||
|
return uriParts[uriParts.length - 1].split("?")[0];
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
showHelp,
|
showHelp,
|
||||||
|
|
@ -140,4 +175,6 @@ module.exports = {
|
||||||
getLastBackupErrorMailSentInMilliSec,
|
getLastBackupErrorMailSentInMilliSec,
|
||||||
getCurrentAppsmithVersion,
|
getCurrentAppsmithVersion,
|
||||||
preprocessMongoDBURI,
|
preprocessMongoDBURI,
|
||||||
|
execCommandSilent,
|
||||||
|
getDatabaseNameFromMongoURI,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
appsmithctl backup --error-mail || exit 1
|
appsmithctl backup --non-interactive --error-mail || exit 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user