diff --git a/src/routes/system/SystemRouter.ts b/src/routes/system/SystemRouter.ts index b58bca8..2d715c7 100644 --- a/src/routes/system/SystemRouter.ts +++ b/src/routes/system/SystemRouter.ts @@ -8,11 +8,27 @@ import SystemRouteSelfHostRegistry = require('./SystemRouteSelfHostRegistry') import CaptainConstants = require('../../utils/CaptainConstants') import InjectionExtractor = require('../../injection/InjectionExtractor') import Utils from '../../utils/Utils' +import * as path from 'path' const router = express.Router() router.use('/selfhostregistry/', SystemRouteSelfHostRegistry) +router.post('/createbackup/', function(req, res, next) { + const backupManager = CaptainManager.get().getBackupManager() + + Promise.resolve() + .then(function() { + return backupManager.createBackup() + }) + .then(function(pathOfBackup) { + res.sendFile(pathOfBackup, {}, function(err) { + backupManager.deleteBackupDirectoryIfExists() + }) + }) + .catch(ApiStatusCodes.createCatcher(res)) +}) + router.post('/changerootdomain/', function(req, res, next) { let requestedCustomDomain = Utils.removeHttpHttps( (req.body.rootDomain || '').toLowerCase() diff --git a/src/user/system/BackupManager.ts b/src/user/system/BackupManager.ts new file mode 100644 index 0000000..0c5f24c --- /dev/null +++ b/src/user/system/BackupManager.ts @@ -0,0 +1,150 @@ +import uuid = require('uuid/v4') +import SshClientImport = require('ssh2') +import request = require('request') +import fs = require('fs-extra') +import CaptainConstants = require('../../utils/CaptainConstants') +import Logger = require('../../utils/Logger') +import LoadBalancerManager = require('./LoadBalancerManager') +import EnvVars = require('../../utils/EnvVars') +import CertbotManager = require('./CertbotManager') +import SelfHostedDockerRegistry = require('./SelfHostedDockerRegistry') +import ApiStatusCodes = require('../../api/ApiStatusCodes') +import DataStoreProvider = require('../../datastore/DataStoreProvider') +import DataStore = require('../../datastore/DataStore') +import DockerApi from '../../docker/DockerApi' +import { IRegistryTypes, IRegistryInfo } from '../../models/IRegistryInfo' +import MigrateCaptainDuckDuck from '../../utils/MigrateCaptainDuckDuck' +import Authenticator = require('../Authenticator') +import * as tar from 'tar' + +const BACKUP_JSON = 'backup.json' + +export interface IBackupCallbacks { + getNodesInfo: () => Promise + getCaptainSalt: () => string +} + +export default class BackupManager { + private longOperationInProgress: boolean + + constructor( + private iBackupCallbacks: IBackupCallbacks, + private certbotManager: CertbotManager + ) { + // + } + + lock() { + if (this.longOperationInProgress) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + 'Another operation is in process for Certbot. Please wait a few seconds and try again.' + ) + } + + this.longOperationInProgress = true + } + + unlock() { + this.longOperationInProgress = false + } + + isRunning() { + return !!this.longOperationInProgress + } + + createBackup() { + const self = this + return Promise.resolve() // + .then(function() { + self.certbotManager.lock() + return self + .createBackupInternal() + .then(function(data) { + self.certbotManager.unlock() + return data + }) + .catch(function(err) { + self.certbotManager.unlock() + throw err + }) + }) + } + + createBackupInternal() { + const self = this + return Promise.resolve() // + .then(function() { + self.lock() + + // Check if exist /captain/temp/backup, delete directory + // Create directory /captain/temp/backup/raw + // Copy /captain/data to .../backup/raw/data + // Ensure .../backup/raw/meta/backup.json + // Create tar file FROM: .../backup/raw/ TO: .../backup/backup.tar + + const RAW = CaptainConstants.captainRootDirectoryBackup + '/raw' + + return Promise.resolve() // + .then(function() { + return self.deleteBackupDirectoryIfExists() + }) + .then(function() { + return fs.ensureDir(RAW) + }) + .then(function() { + return fs.copy( + CaptainConstants.captainDataDirectory, + RAW + '/data', + { preserveTimestamps: true } + ) + }) + .then(function() { + return self.iBackupCallbacks.getNodesInfo() + }) + .then(function(nodes) { + return fs.outputJson(RAW + '/meta/' + BACKUP_JSON, { + salt: self.iBackupCallbacks.getCaptainSalt(), + nodes: nodes, + }) + }) + .then(function() { + const tarFilePath = + CaptainConstants.captainRootDirectoryBackup + + '/backup.tar' + return tar + .c( + { + file: tarFilePath, + cwd: RAW, + }, + ['./'] + ) + .then(function() { + return tarFilePath + }) + }) + .then(function(tarFilePath) { + self.unlock() + return tarFilePath + }) + .catch(function(err) { + self.unlock() + throw err + }) + }) + } + + deleteBackupDirectoryIfExists() { + return Promise.resolve() // + .then(function() { + if ( + fs.existsSync(CaptainConstants.captainRootDirectoryBackup) + ) { + return fs.remove( + CaptainConstants.captainRootDirectoryBackup + ) + } + }) + } +} diff --git a/src/user/system/CaptainManager.ts b/src/user/system/CaptainManager.ts index 31b3c52..9ad5947 100644 --- a/src/user/system/CaptainManager.ts +++ b/src/user/system/CaptainManager.ts @@ -15,6 +15,7 @@ import DockerApi from '../../docker/DockerApi' import { IRegistryTypes, IRegistryInfo } from '../../models/IRegistryInfo' import MigrateCaptainDuckDuck from '../../utils/MigrateCaptainDuckDuck' import Authenticator = require('../Authenticator') +import BackupManager from './BackupManager' const DEBUG_SALT = 'THIS IS NOT A REAL CERTIFICATE' const SshClient = SshClientImport.Client @@ -33,6 +34,7 @@ class CaptainManager { private certbotManager: CertbotManager private loadBalancerManager: LoadBalancerManager private dockerRegistry: SelfHostedDockerRegistry + private backupManager: BackupManager private myNodeId: string | undefined private inited: boolean private waitUntilRestarted: boolean @@ -60,6 +62,7 @@ class CaptainManager { this.captainSalt = '' this.consecutiveHealthCheckFailCount = 0 this.healthCheckUuid = uuid() + this.backupManager = new BackupManager(this, this.certbotManager) } initialize() { @@ -405,8 +408,16 @@ class CaptainManager { return this.healthCheckUuid } + getBackupManager() { + return this.backupManager + } + isInitialized() { - return this.inited && !this.waitUntilRestarted + return ( + this.inited && + !this.waitUntilRestarted && + !this.backupManager.isRunning() + ) } getCaptainImageTags() { @@ -978,6 +989,7 @@ class CaptainManager { resetSelf() { const self = this Logger.d('Captain is resetting itself!') + self.waitUntilRestarted = true return new Promise(function(resolve, reject) { setTimeout(function() { const promiseToIgnore = self.dockerApi.updateService( diff --git a/src/user/system/CertbotManager.ts b/src/user/system/CertbotManager.ts index d2a24c1..43334ab 100644 --- a/src/user/system/CertbotManager.ts +++ b/src/user/system/CertbotManager.ts @@ -210,19 +210,27 @@ class CertbotManager { }) } + lock() { + if (this.isOperationInProcess) { + throw ApiStatusCodes.createError( + ApiStatusCodes.STATUS_ERROR_GENERIC, + 'Another operation is in process for Certbot. Please wait a few seconds and try again.' + ) + } + + this.isOperationInProcess = true + } + + unlock() { + this.isOperationInProcess = false + } + runCommand(cmd: string[]) { const dockerApi = this.dockerApi const self = this return Promise.resolve().then(function() { - if (self.isOperationInProcess) { - throw ApiStatusCodes.createError( - ApiStatusCodes.STATUS_ERROR_GENERIC, - 'Another operation is in process for Certbot. Please wait a few seconds and try again.' - ) - } - - self.isOperationInProcess = true + self.lock() const nonInterActiveCommand = [...cmd, '--non-interactive'] return dockerApi @@ -231,11 +239,11 @@ class CertbotManager { nonInterActiveCommand ) .then(function(data) { - self.isOperationInProcess = false + self.unlock() return data }) .catch(function(error) { - self.isOperationInProcess = false + self.unlock() throw error }) }) @@ -286,6 +294,9 @@ class CertbotManager { .then(function(output) { // Ignore output :) }) + .catch(function(err) { + Logger.e(err) + }) } init(myNodeId: string) { diff --git a/src/utils/CaptainConstants.ts b/src/utils/CaptainConstants.ts index e52681e..7f4f0f6 100644 --- a/src/utils/CaptainConstants.ts +++ b/src/utils/CaptainConstants.ts @@ -79,6 +79,8 @@ let data = { captainRootDirectoryTemp: CAPTAIN_ROOT_DIRECTORY_TEMP, + captainRootDirectoryBackup: CAPTAIN_ROOT_DIRECTORY_TEMP + '/backup', + captainRawSourceDirectoryBase: CAPTAIN_ROOT_DIRECTORY_TEMP + '/image_raw', captainRootDirectoryGenerated: CAPTAIN_ROOT_DIRECTORY_GENERATED,