2015-07-20 00:09:47 -07:00
#!/usr/bin/env node
'use strict' ;
exports = module . exports = {
2021-02-24 14:47:05 -08:00
run ,
2019-08-26 15:55:57 -07:00
2015-07-20 00:09:47 -07:00
// exported for testing
2018-09-14 10:18:43 -07:00
_createAppDir : createAppDir ,
_deleteAppDir : deleteAppDir ,
2015-07-20 00:09:47 -07:00
_verifyManifest : verifyManifest ,
} ;
2021-08-20 09:19:44 -07:00
const apps = require ( './apps.js' ) ,
2023-08-04 15:34:38 +05:30
appstore = require ( './appstore.js' ) ,
2015-07-20 00:09:47 -07:00
assert = require ( 'assert' ) ,
2021-09-30 09:50:30 -07:00
AuditSource = require ( './auditsource.js' ) ,
2023-08-15 20:24:54 +05:30
backups = require ( './backups.js' ) ,
2021-07-14 11:07:19 -07:00
backuptask = require ( './backuptask.js' ) ,
2019-09-05 17:13:07 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-26 10:10:14 -07:00
constants = require ( './constants.js' ) ,
2015-07-20 00:09:47 -07:00
debug = require ( 'debug' ) ( 'box:apptask' ) ,
2022-10-18 19:32:07 +02:00
df = require ( './df.js' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( './dns.js' ) ,
2015-10-19 14:09:20 -07:00
docker = require ( './docker.js' ) ,
2015-07-20 00:09:47 -07:00
ejs = require ( 'ejs' ) ,
fs = require ( 'fs' ) ,
2020-11-20 14:13:16 -08:00
iputils = require ( './iputils.js' ) ,
2015-07-20 00:09:47 -07:00
manifestFormat = require ( 'cloudron-manifestformat' ) ,
2021-09-29 20:16:06 -07:00
mounts = require ( './mounts.js' ) ,
2017-08-11 22:05:31 +02:00
os = require ( 'os' ) ,
2015-07-20 00:09:47 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2021-08-25 19:41:46 -07:00
promiseRetry = require ( './promise-retry.js' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2015-07-20 00:09:47 -07:00
safe = require ( 'safetydance' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'apptask' ) ,
2025-02-13 14:03:25 +01:00
_ = require ( './underscore.js' ) ;
2015-07-20 00:09:47 -07:00
2022-04-27 18:41:20 -07:00
const MV _VOLUME _CMD = path . join ( _ _dirname , 'scripts/mvvolume.sh' ) ,
2017-08-11 22:05:31 +02:00
LOGROTATE _CONFIG _EJS = fs . readFileSync ( _ _dirname + '/logrotate.ejs' , { encoding : 'utf8' } ) ,
2018-09-13 13:55:49 -07:00
CONFIGURE _LOGROTATE _CMD = path . join ( _ _dirname , 'scripts/configurelogrotate.sh' ) ;
2015-07-20 00:09:47 -07:00
2019-09-24 10:28:50 -07:00
function makeTaskError ( error , app ) {
2022-08-08 13:50:26 +02:00
assert ( error instanceof BoxError ) ;
2019-12-05 09:32:45 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-24 10:28:50 -07:00
// track a few variables which helps 'repair' restart the task (see also scheduleTask in apps.js)
2019-11-22 14:27:41 -08:00
error . details . taskId = app . taskId ;
error . details . installationState = app . installationState ;
return error . toPlainObject ( ) ;
2019-09-21 19:45:55 -07:00
}
2017-12-21 01:04:38 -08:00
// updates the app object and the database
2021-08-25 19:41:46 -07:00
async function updateApp ( app , values ) {
2017-12-21 01:04:38 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof values , 'object' ) ;
2021-08-25 19:41:46 -07:00
await apps . update ( app . id , values ) ;
2017-12-21 01:04:38 -08:00
2021-08-25 19:41:46 -07:00
for ( const value in values ) {
app [ value ] = values [ value ] ;
}
2017-12-21 01:04:38 -08:00
}
2021-08-25 19:41:46 -07:00
async function allocateContainerIp ( app ) {
2020-11-20 14:13:16 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2022-06-09 13:57:57 +02:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) return ;
2022-06-06 20:04:22 +02:00
2021-12-07 11:18:26 -08:00
await promiseRetry ( { times : 10 , interval : 0 , debug } , async function ( ) {
2023-02-21 12:03:58 +01:00
const iprange = iputils . intFromIp ( constants . APPS _IPv4 _END ) - iputils . intFromIp ( constants . APPS _IPv4 _START ) ;
2024-06-06 11:33:42 +02:00
const rnd = Math . floor ( Math . random ( ) * iprange ) ;
2023-02-21 12:03:58 +01:00
const containerIp = iputils . ipFromInt ( iputils . intFromIp ( constants . APPS _IPv4 _START ) + rnd ) ;
2022-04-14 15:40:51 -05:00
await updateApp ( app , { containerIp } ) ;
2021-08-25 19:41:46 -07:00
} ) ;
2020-11-20 14:13:16 -08:00
}
2021-08-25 19:41:46 -07:00
async function createContainer ( app ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2015-10-19 16:22:35 -07:00
assert ( ! app . containerId ) ; // otherwise, it will trigger volumeFrom
2022-06-09 13:57:57 +02:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) return ;
2022-06-06 20:04:22 +02:00
2021-09-25 21:38:09 -07:00
debug ( 'createContainer: creating container' ) ;
2015-10-19 18:48:56 -07:00
2021-08-25 19:41:46 -07:00
const container = await docker . createContainer ( app ) ;
2015-07-20 00:09:47 -07:00
2021-08-25 19:41:46 -07:00
await updateApp ( app , { containerId : container . id } ) ;
2019-11-19 17:27:39 -08:00
2021-08-25 19:41:46 -07:00
// re-generate configs that rely on container id
await addLogrotateConfig ( app ) ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
async function deleteContainers ( app , options ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-01-17 23:32:24 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2016-01-05 12:14:39 +01:00
2021-09-25 21:38:09 -07:00
debug ( 'deleteContainer: deleting app containers (app, scheduler)' ) ;
2015-10-19 18:48:56 -07:00
2021-09-16 13:59:03 -07:00
// remove configs that rely on container id
await removeLogrotateConfig ( app ) ;
await docker . stopContainers ( app . id ) ;
await docker . deleteContainers ( app . id , options ) ;
await updateApp ( app , { containerId : null } ) ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
async function createAppDir ( app ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-06-06 15:16:01 +02:00
// we have to create app dir regardless of localstorage addon. this dir is used as a temp space for backup dumps
2019-09-06 11:19:58 -07:00
const appDir = path . join ( paths . APPS _DATA _DIR , app . id ) ;
2021-09-16 13:59:03 -07:00
const [ error ] = await safe ( fs . promises . mkdir ( appDir , { recursive : true } ) ) ;
2024-10-30 15:43:43 +01:00
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Error creating directory: ${ error . message } ` ) ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
async function deleteAppDir ( app , options ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2017-09-07 15:43:57 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2016-01-05 12:14:39 +01:00
2018-09-15 17:05:04 -07:00
const appDataDir = path . join ( paths . APPS _DATA _DIR , app . id ) ;
2018-09-13 13:55:49 -07:00
2018-10-18 19:47:40 -07:00
// resolve any symlinked data dir
const stat = safe . fs . lstatSync ( appDataDir ) ;
2021-09-16 13:59:03 -07:00
if ( ! stat ) return ;
2018-09-14 14:37:20 +02:00
2018-10-18 19:47:40 -07:00
const resolvedAppDataDir = stat . isSymbolicLink ( ) ? safe . fs . readlinkSync ( appDataDir ) : appDataDir ;
2018-09-13 13:55:49 -07:00
2018-10-18 19:47:40 -07:00
if ( safe . fs . existsSync ( resolvedAppDataDir ) ) {
const entries = safe . fs . readdirSync ( resolvedAppDataDir ) ;
2021-09-16 13:59:03 -07:00
if ( ! entries ) throw new BoxError ( BoxError . FS _ERROR , ` Error listing ${ resolvedAppDataDir } : ${ safe . error . message } ` ) ;
2018-10-18 19:47:40 -07:00
// remove only files. directories inside app dir are currently volumes managed by the addons
2019-01-18 14:48:31 -08:00
// we cannot delete those dirs anyway because of perms
2018-10-18 19:47:40 -07:00
entries . forEach ( function ( entry ) {
2024-06-06 11:33:42 +02:00
const stat = safe . fs . statSync ( path . join ( resolvedAppDataDir , entry ) ) ;
2018-10-18 19:47:40 -07:00
if ( stat && ! stat . isDirectory ( ) ) safe . fs . unlinkSync ( path . join ( resolvedAppDataDir , entry ) ) ;
} ) ;
}
2018-09-15 17:05:04 -07:00
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
2018-10-18 19:47:40 -07:00
if ( options . removeDirectory ) {
if ( stat . isSymbolicLink ( ) ) {
2019-09-06 11:19:58 -07:00
if ( ! safe . fs . unlinkSync ( appDataDir ) ) {
2021-09-16 13:59:03 -07:00
if ( safe . error . code !== 'ENOENT' ) throw new BoxError ( BoxError . FS _ERROR , ` Error unlinking dir ${ appDataDir } : ${ safe . error . message } ` ) ;
2019-09-06 11:19:58 -07:00
}
2018-10-18 19:47:40 -07:00
} else {
2022-02-16 20:30:33 -08:00
if ( ! safe . fs . rmSync ( appDataDir , { recursive : true } ) ) {
2021-09-16 13:59:03 -07:00
if ( safe . error . code !== 'ENOENT' ) throw new BoxError ( BoxError . FS _ERROR , ` Error removing dir ${ appDataDir } : ${ safe . error . message } ` ) ;
2019-09-06 11:19:58 -07:00
}
2018-10-18 19:47:40 -07:00
}
}
2015-07-20 00:09:47 -07:00
}
2021-08-25 19:41:46 -07:00
async function addLogrotateConfig ( app ) {
2017-08-11 22:05:31 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-08-25 19:41:46 -07:00
const result = await docker . inspect ( app . containerId ) ;
2017-08-11 22:05:31 +02:00
2021-08-25 19:41:46 -07:00
const runVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/run' ; } ) ;
if ( ! runVolume ) throw new BoxError ( BoxError . DOCKER _ERROR , 'App does not have /run mounted' ) ;
2017-08-11 22:05:31 +02:00
2021-08-25 19:41:46 -07:00
// logrotate configs can have arbitrary commands, so the config files must be owned by root
const logrotateConf = ejs . render ( LOGROTATE _CONFIG _EJS , { volumePath : runVolume . Source , appId : app . id } ) ;
const tmpFilePath = path . join ( os . tmpdir ( ) , app . id + '.logrotate' ) ;
2019-09-06 11:19:58 -07:00
2021-08-25 19:41:46 -07:00
safe . fs . writeFileSync ( tmpFilePath , logrotateConf ) ;
if ( safe . error ) throw new BoxError ( BoxError . FS _ERROR , ` Error writing logrotate config: ${ safe . error . message } ` ) ;
2019-09-06 11:19:58 -07:00
2024-10-14 19:10:31 +02:00
const [ error ] = await safe ( shell . promises . sudo ( [ CONFIGURE _LOGROTATE _CMD , 'add' , app . id , tmpFilePath ] , { } ) ) ;
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . LOGROTATE _ERROR , ` Error adding logrotate config: ${ error . message } ` ) ;
2017-08-11 22:05:31 +02:00
}
2021-08-25 19:41:46 -07:00
async function removeLogrotateConfig ( app ) {
2017-08-11 22:05:31 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-10-14 19:10:31 +02:00
const [ error ] = await safe ( shell . promises . sudo ( [ CONFIGURE _LOGROTATE _CMD , 'remove' , app . id ] , { } ) ) ;
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . LOGROTATE _ERROR , ` Error removing logrotate config: ${ error . message } ` ) ;
2019-09-06 11:19:58 -07:00
}
2021-09-16 13:59:03 -07:00
async function cleanupLogs ( app ) {
2019-09-06 11:19:58 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
// note that redis container logs are cleaned up by the addon
2021-09-16 13:59:03 -07:00
const [ error ] = await safe ( fs . promises . rm ( path . join ( paths . LOG _DIR , app . id ) , { force : true , recursive : true } ) ) ;
2023-04-16 10:49:59 +02:00
if ( error ) debug ( 'cleanupLogs: cannot cleanup logs: %o' , error ) ;
2017-08-11 22:05:31 +02:00
}
2021-09-16 13:59:03 -07:00
async function verifyManifest ( manifest ) {
2017-10-12 17:46:15 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2016-01-05 12:14:39 +01:00
2021-09-16 13:59:03 -07:00
let error = manifestFormat . parse ( manifest ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` Manifest error: ${ error . message } ` ) ;
2015-07-20 00:09:47 -07:00
2024-03-30 18:51:19 +01:00
error = await apps . checkManifest ( manifest ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . CONFLICT , ` Manifest constraint check failed: ${ error . message } ` ) ;
2015-07-20 00:09:47 -07:00
}
2021-08-25 19:41:46 -07:00
async function downloadIcon ( app ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2017-01-19 12:38:41 +01:00
// nothing to download if we dont have an appStoreId
2021-08-25 19:41:46 -07:00
if ( ! app . appStoreId ) return ;
2017-01-19 12:38:41 +01:00
2021-09-25 21:38:09 -07:00
debug ( ` downloadIcon: Downloading icon of ${ app . appStoreId } @ ${ app . manifest . version } ` ) ;
2015-07-20 00:09:47 -07:00
2023-08-04 15:34:38 +05:30
const appStoreIcon = await appstore . downloadIcon ( app . appStoreId , app . manifest . version ) ;
await updateApp ( app , { appStoreIcon } ) ;
2015-07-20 00:09:47 -07:00
}
2022-06-01 22:44:52 -07:00
async function moveDataDir ( app , targetVolumeId , targetVolumePrefix ) {
2018-12-20 14:33:29 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-06-06 15:22:33 +02:00
assert . ok ( app . manifest . addons . localstorage , 'should have local storage addon' ) ;
2022-06-01 22:44:52 -07:00
assert ( targetVolumeId === null || typeof targetVolumeId === 'string' ) ;
assert ( targetVolumePrefix === null || typeof targetVolumePrefix === 'string' ) ;
2018-12-20 14:33:29 -08:00
2022-06-01 22:44:52 -07:00
const resolvedSourceDir = await apps . getStorageDir ( app ) ;
2023-05-25 11:27:23 +02:00
const resolvedTargetDir = await apps . getStorageDir ( Object . assign ( { } , app , { storageVolumeId : targetVolumeId , storageVolumePrefix : targetVolumePrefix } ) ) ;
2018-12-20 14:33:29 -08:00
2021-09-25 21:38:09 -07:00
debug ( ` moveDataDir: migrating data from ${ resolvedSourceDir } to ${ resolvedTargetDir } ` ) ;
2018-12-20 14:33:29 -08:00
2022-06-08 11:40:40 -07:00
if ( resolvedSourceDir !== resolvedTargetDir ) {
2024-10-14 19:10:31 +02:00
const [ error ] = await safe ( shell . promises . sudo ( [ MV _VOLUME _CMD , resolvedSourceDir , resolvedTargetDir ] , { } ) ) ;
2022-06-08 11:40:40 -07:00
if ( error ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Error migrating data directory: ${ error . message } ` ) ;
}
2019-12-06 08:09:43 -08:00
2022-06-08 11:40:40 -07:00
await updateApp ( app , { storageVolumeId : targetVolumeId , storageVolumePrefix : targetVolumePrefix } ) ;
2018-12-20 14:33:29 -08:00
}
2021-08-25 19:41:46 -07:00
async function downloadImage ( manifest ) {
2019-08-12 20:38:24 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2022-06-06 20:04:22 +02:00
// skip for relay app
2022-06-09 13:57:57 +02:00
if ( manifest . id === constants . PROXY _APP _APPSTORE _ID ) return ;
2022-06-06 20:04:22 +02:00
2021-08-25 19:41:46 -07:00
const info = await docker . info ( ) ;
const [ dfError , diskUsage ] = await safe ( df . file ( info . DockerRootDir ) ) ;
if ( dfError ) throw new BoxError ( BoxError . FS _ERROR , ` Error getting file system info: ${ dfError . message } ` ) ;
2019-08-12 20:38:24 -07:00
2024-10-30 16:21:21 +01:00
if ( diskUsage . available < ( 1024 * 1024 * 1024 ) ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Not enough disk space to pull docker image. available: ${ diskUsage . available } ` ) ;
2019-09-06 11:19:58 -07:00
2021-08-25 19:41:46 -07:00
await docker . downloadImage ( manifest ) ;
2019-08-12 20:38:24 -07:00
}
2024-09-07 09:37:34 +02:00
async function updateChecklist ( app , newChecks , acknowledged = false ) {
2024-04-17 18:29:47 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof newChecks , 'object' ) ;
2024-09-07 09:37:34 +02:00
assert . strictEqual ( typeof acknowledged , 'boolean' ) ;
2024-04-17 18:29:47 +02:00
// add new checklist items depending on sso state
const checklist = app . checklist || { } ;
2024-06-06 11:33:42 +02:00
for ( const k in newChecks ) {
2024-04-19 14:29:41 +02:00
if ( app . checklist [ k ] ) continue ;
2024-04-17 18:29:47 +02:00
const item = {
2024-09-07 09:37:34 +02:00
acknowledged : acknowledged ,
2024-04-17 18:29:47 +02:00
sso : newChecks [ k ] . sso ,
2024-06-24 18:39:37 +02:00
appVersion : app . version ,
message : newChecks [ k ] . message ,
changedAt : 0 ,
changedBy : null , // username from audit log
2024-04-17 18:29:47 +02:00
} ;
if ( typeof item . sso === 'undefined' ) checklist [ k ] = item ;
else if ( item . sso && app . sso ) checklist [ k ] = item ;
else if ( ! item . sso && ! app . sso ) checklist [ k ] = item ;
}
await updateApp ( app , { checklist } ) ;
}
2021-08-25 19:41:46 -07:00
async function startApp ( app ) {
2021-09-25 21:38:09 -07:00
debug ( 'startApp: starting container' ) ;
2020-11-20 11:17:24 -08:00
2021-08-25 19:41:46 -07:00
if ( app . runState === apps . RSTATE _STOPPED ) return ;
2019-09-22 00:20:12 -07:00
2022-06-06 20:04:22 +02:00
// skip for relay app
2022-06-09 13:57:57 +02:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) return ;
2022-06-06 20:04:22 +02:00
2021-08-25 19:41:46 -07:00
await docker . startContainer ( app . id ) ;
2019-09-22 00:20:12 -07:00
}
2025-06-11 14:19:31 +02:00
async function installCommand ( app , args , progressCallback ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-16 09:31:34 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 20:43:15 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-01-05 12:14:39 +01:00
2019-09-30 09:30:49 -07:00
const restoreConfig = args . restoreConfig ; // has to be set when restoring
2019-09-16 09:31:34 -07:00
const overwriteDns = args . overwriteDns ;
2021-02-24 14:47:05 -08:00
const skipDnsSetup = args . skipDnsSetup ;
2019-12-06 09:42:05 -08:00
const oldManifest = args . oldManifest ;
2017-04-11 12:49:21 -07:00
2021-09-16 13:59:03 -07:00
// this protects against the theoretical possibility of an app being marked for install/restore from
// a previous version of box code
await verifyManifest ( app . manifest ) ;
// teardown for re-installs
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Deleting old containers' } ) ;
2021-09-16 13:59:03 -07:00
await reverseProxy . unconfigureApp ( app ) ;
await deleteContainers ( app , { managedOnly : true } ) ;
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
let addonsToRemove ;
if ( oldManifest ) {
addonsToRemove = _ . omit ( oldManifest . addons , Object . keys ( app . manifest . addons ) ) ;
} else {
addonsToRemove = app . manifest . addons ;
}
await services . teardownAddons ( app , addonsToRemove ) ;
2018-05-22 12:05:55 -07:00
2022-04-05 09:28:30 -07:00
if ( ! restoreConfig || restoreConfig . remotePath ) { // in-place import should not delete data dir
2021-09-16 13:59:03 -07:00
await deleteAppDir ( app , { removeDirectory : false } ) ; // do not remove any symlinked appdata dir
}
2017-04-11 12:49:21 -07:00
2024-07-08 12:54:20 +02:00
if ( oldManifest && oldManifest . dockerImage !== app . manifest . dockerImage ) {
2024-12-14 14:53:08 +01:00
await docker . deleteImage ( oldManifest . dockerImage ) ;
2021-09-16 13:59:03 -07:00
}
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
// allocating container ip here, lets the users "repair" an app if allocation fails at apps.add time
await allocateContainerIp ( app ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 20 , message : 'Downloading icon' } ) ;
await downloadIcon ( app ) ;
2021-02-24 16:57:47 -08:00
2024-04-18 17:35:01 +02:00
await progressCallback ( { percent : 25 , message : 'Updating checklist' } ) ;
2024-09-16 13:46:19 +02:00
await updateChecklist ( app , app . manifest . checklist || { } , restoreConfig ? true : false ) ;
2024-04-18 17:35:01 +02:00
2021-09-16 13:59:03 -07:00
if ( ! skipDnsSetup ) {
await progressCallback ( { percent : 30 , message : 'Registering subdomains' } ) ;
2015-07-20 00:09:47 -07:00
2022-01-16 12:32:12 -08:00
await dns . registerLocations ( [ { subdomain : app . subdomain , domain : app . domain } ] . concat ( app . secondaryDomains ) . concat ( app . redirectDomains ) . concat ( app . aliasDomains ) , { overwriteDns } , progressCallback ) ;
2021-09-16 13:59:03 -07:00
}
2020-11-20 14:13:16 -08:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 40 , message : 'Downloading image' } ) ;
await downloadImage ( app . manifest ) ;
await progressCallback ( { percent : 50 , message : 'Creating app data directory' } ) ;
await createAppDir ( app ) ;
2021-09-29 20:16:06 -07:00
if ( ! restoreConfig ) { // install
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Setting up addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
2022-04-05 09:28:30 -07:00
} else if ( app . installationState === apps . ISTATE _PENDING _IMPORT && ! restoreConfig . remotePath ) { // in-place import
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Importing addons in-place' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
2025-02-13 14:03:25 +01:00
await services . clearAddons ( app , _ . omit ( app . manifest . addons , [ 'localstorage' ] ) ) ;
2024-02-10 11:53:25 +01:00
await apps . loadConfig ( app ) ;
2021-09-16 13:59:03 -07:00
await services . restoreAddons ( app , app . manifest . addons ) ;
2021-09-29 20:16:06 -07:00
} else if ( app . installationState === apps . ISTATE _PENDING _IMPORT ) { // import
await progressCallback ( { percent : 65 , message : 'Downloading backup and restoring addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
await services . clearAddons ( app , app . manifest . addons ) ;
2023-04-30 08:14:17 +02:00
const backupConfig = restoreConfig . backupConfig ;
2024-06-25 12:38:27 +02:00
const mountObject = await backups . setupManagedStorage ( backupConfig , ` /mnt/appimport- ${ app . id } ` ) ;
2023-08-15 20:24:54 +05:30
if ( mountObject ) await progressCallback ( { percent : 70 , message : 'Setting up mount for importing' } ) ;
backupConfig . rootPath = backups . getRootPath ( backupConfig , ` /mnt/appimport- ${ app . id } ` ) ;
2021-09-29 20:16:06 -07:00
await backuptask . downloadApp ( app , restoreConfig , ( progress ) => { progressCallback ( { percent : 75 , message : progress . message } ) ; } ) ;
2024-02-10 11:53:25 +01:00
await apps . loadConfig ( app ) ;
2021-09-29 20:16:06 -07:00
if ( mountObject ) await mounts . removeMount ( mountObject ) ;
await progressCallback ( { percent : 75 , message : 'Restoring addons' } ) ;
await services . restoreAddons ( app , app . manifest . addons ) ;
} else { // clone and restore
await progressCallback ( { percent : 65 , message : 'Downloading backup and restoring addons' } ) ;
2021-09-16 13:59:03 -07:00
await services . setupAddons ( app , app . manifest . addons ) ;
await services . clearAddons ( app , app . manifest . addons ) ;
await backuptask . downloadApp ( app , restoreConfig , ( progress ) => { progressCallback ( { percent : 65 , message : progress . message } ) ; } ) ;
2024-12-10 22:43:06 +01:00
if ( app . installationState === apps . ISTATE _PENDING _CLONE ) {
const customIcon = safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) ) ;
if ( customIcon ) await updateApp ( app , { icon : customIcon } ) ;
}
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 70 , message : 'Restoring addons' } ) ;
await services . restoreAddons ( app , app . manifest . addons ) ;
}
await progressCallback ( { percent : 80 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
await startApp ( app ) ;
if ( ! skipDnsSetup ) {
await progressCallback ( { percent : 85 , message : 'Waiting for DNS propagation' } ) ;
2023-08-14 09:40:31 +05:30
await dns . waitForLocations ( [ { subdomain : app . subdomain , domain : app . domain } ] . concat ( app . secondaryDomains ) . concat ( app . redirectDomains ) . concat ( app . aliasDomains ) , progressCallback ) ;
2021-09-16 13:59:03 -07:00
}
await progressCallback ( { percent : 95 , message : 'Configuring reverse proxy' } ) ;
2021-09-30 09:50:30 -07:00
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2015-07-20 00:09:47 -07:00
}
2025-06-11 14:19:31 +02:00
async function backupCommand ( app , args , progressCallback ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 22:11:27 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-01-05 12:14:39 +01:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 10 , message : 'Backing up' } ) ;
2021-09-30 10:45:25 -07:00
const backupId = await backuptask . backupApp ( app , { snapshotOnly : ! ! args . snapshotOnly } , ( progress ) => {
2021-09-16 13:59:03 -07:00
progressCallback ( { percent : 30 , message : progress . message } ) ;
2021-09-30 10:45:25 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null } ) ;
2021-09-30 10:45:25 -07:00
return backupId ;
2015-07-20 00:09:47 -07:00
}
2025-06-11 14:19:31 +02:00
async function createCommand ( app , args , progressCallback ) {
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Deleting old container' } ) ;
2021-09-16 13:59:03 -07:00
await deleteContainers ( app , { managedOnly : true } ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
// FIXME: re-setup addons only because sendmail addon to re-inject env vars on mailboxName change
await progressCallback ( { percent : 30 , message : 'Setting up addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
2019-09-09 15:58:58 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await startApp ( app ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-09-08 16:57:08 -07:00
}
2025-06-11 14:19:31 +02:00
async function changeLocationCommand ( app , args , progressCallback ) {
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-10 15:23:47 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2019-09-10 15:23:47 -07:00
const oldConfig = args . oldConfig ;
2019-09-08 16:57:08 -07:00
const locationChanged = oldConfig . fqdn !== app . fqdn ;
2021-02-24 14:47:05 -08:00
const skipDnsSetup = args . skipDnsSetup ;
2019-09-26 22:30:58 -07:00
const overwriteDns = args . overwriteDns ;
2019-09-08 16:57:08 -07:00
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Unregistering old domains' } ) ;
2021-09-16 13:59:03 -07:00
await reverseProxy . unconfigureApp ( app ) ;
await deleteContainers ( app , { managedOnly : true } ) ;
2021-01-18 17:26:26 -08:00
2021-09-16 13:59:03 -07:00
// unregister old domains
2022-01-14 22:40:51 -08:00
let obsoleteDomains = [ ] ;
if ( oldConfig . secondaryDomains ) {
obsoleteDomains = obsoleteDomains . concat ( oldConfig . secondaryDomains . filter ( function ( o ) {
return ! app . secondaryDomains . some ( function ( n ) { return n . subdomain === o . subdomain && n . domain === o . domain ; } ) ;
} ) ) ;
}
if ( oldConfig . redirectDomains ) {
obsoleteDomains = obsoleteDomains . concat ( oldConfig . redirectDomains . filter ( function ( o ) {
return ! app . redirectDomains . some ( function ( n ) { return n . subdomain === o . subdomain && n . domain === o . domain ; } ) ;
} ) ) ;
}
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
if ( oldConfig . aliasDomains ) {
obsoleteDomains = obsoleteDomains . concat ( oldConfig . aliasDomains . filter ( function ( o ) {
return ! app . aliasDomains . some ( function ( n ) { return n . subdomain === o . subdomain && n . domain === o . domain ; } ) ;
} ) ) ;
}
2019-09-08 16:57:08 -07:00
2022-01-16 12:32:12 -08:00
if ( locationChanged ) obsoleteDomains . push ( { subdomain : oldConfig . subdomain , domain : oldConfig . domain } ) ;
2021-02-24 14:47:05 -08:00
2021-09-16 13:59:03 -07:00
if ( obsoleteDomains . length !== 0 ) await dns . unregisterLocations ( obsoleteDomains , progressCallback ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
// setup dns
if ( ! skipDnsSetup ) {
await progressCallback ( { percent : 30 , message : 'Registering subdomains' } ) ;
2022-01-16 12:32:12 -08:00
await dns . registerLocations ( [ { subdomain : app . subdomain , domain : app . domain } ] . concat ( app . secondaryDomains ) . concat ( app . redirectDomains ) . concat ( app . aliasDomains ) , { overwriteDns } , progressCallback ) ;
2021-09-16 13:59:03 -07:00
}
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
// re-setup addons since they rely on the app's fqdn (e.g oauth)
await progressCallback ( { percent : 50 , message : 'Setting up addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await startApp ( app ) ;
2021-02-24 16:57:47 -08:00
2021-09-16 13:59:03 -07:00
if ( ! skipDnsSetup ) {
await progressCallback ( { percent : 80 , message : 'Waiting for DNS propagation' } ) ;
2023-08-14 09:40:31 +05:30
await dns . waitForLocations ( [ { subdomain : app . subdomain , domain : app . domain } ] . concat ( app . secondaryDomains ) . concat ( app . redirectDomains ) . concat ( app . aliasDomains ) , progressCallback ) ;
2021-09-16 13:59:03 -07:00
}
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 90 , message : 'Configuring reverse proxy' } ) ;
2021-09-30 09:50:30 -07:00
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2020-11-20 14:13:16 -08:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-09-08 16:57:08 -07:00
}
2025-06-11 14:19:31 +02:00
async function changeServicesCommand ( app , args , progressCallback ) {
2023-07-13 15:06:07 +05:30
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof args , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Deleting old containers' } ) ;
2023-07-13 15:06:07 +05:30
await deleteContainers ( app , { managedOnly : true } ) ;
const unusedAddons = { } ;
2024-06-06 15:19:19 +02:00
if ( app . manifest . addons ? . turn && ! app . enableTurn ) unusedAddons . turn = app . manifest . addons . turn ;
if ( app . manifest . addons ? . sendmail && ! app . enableMailbox ) unusedAddons . sendmail = app . manifest . addons . sendmail ;
if ( app . manifest . addons ? . recvmail && ! app . enableInbox ) unusedAddons . recvmail = app . manifest . addons . recvmail ;
if ( app . manifest . addons ? . redis && ! app . enableRedis ) unusedAddons . redis = app . manifest . addons . redis ;
2023-07-13 16:37:33 +05:30
2023-07-13 15:06:07 +05:30
await progressCallback ( { percent : 20 , message : 'Removing unused addons' } ) ;
await services . teardownAddons ( app , unusedAddons ) ;
await progressCallback ( { percent : 40 , message : 'Setting up addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
await progressCallback ( { percent : 60 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
await startApp ( app ) ;
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
}
2025-06-11 14:19:31 +02:00
async function migrateDataDirCommand ( app , args , progressCallback ) {
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2022-06-01 22:44:52 -07:00
const { newStorageVolumeId , newStorageVolumePrefix } = args ;
assert ( newStorageVolumeId === null || typeof newStorageVolumeId === 'string' ) ;
assert ( newStorageVolumePrefix === null || typeof newStorageVolumePrefix === 'string' ) ;
2019-09-21 19:45:55 -07:00
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Deleting old containers' } ) ;
2021-09-16 13:59:03 -07:00
await deleteContainers ( app , { managedOnly : true } ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 45 , message : 'Ensuring app data directory' } ) ;
await createAppDir ( app ) ;
2019-09-08 16:57:08 -07:00
2022-06-01 22:44:52 -07:00
// re-setup addons since this creates the localStorage destination
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 50 , message : 'Setting up addons' } ) ;
2023-05-25 11:27:23 +02:00
await services . setupAddons ( Object . assign ( { } , app , { storageVolumeId : newStorageVolumeId , storageVolumePrefix : newStorageVolumePrefix } ) , app . manifest . addons ) ;
2019-09-09 16:37:59 -07:00
2024-06-06 15:22:33 +02:00
if ( app . manifest . addons ? . localstorage ) {
await progressCallback ( { percent : 60 , message : 'Moving data dir' } ) ;
await moveDataDir ( app , newStorageVolumeId , newStorageVolumePrefix ) ;
}
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 90 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await startApp ( app ) ;
2019-09-08 16:57:08 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
2022-06-08 11:40:40 -07:00
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-09-08 16:57:08 -07:00
}
2019-12-06 09:23:58 -08:00
// configure is called for an infra update and repair to re-create container, reverseproxy config. it's all "local"
2025-06-11 14:19:31 +02:00
async function configureCommand ( app , args , progressCallback ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 21:36:18 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-01-05 12:14:39 +01:00
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 10 , message : 'Deleting old containers' } ) ;
2021-09-16 13:59:03 -07:00
await reverseProxy . unconfigureApp ( app ) ;
2021-09-25 21:39:45 -07:00
await deleteContainers ( app , { managedOnly : true } ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 20 , message : 'Downloading icon' } ) ;
await downloadIcon ( app ) ;
2017-03-15 20:24:03 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 40 , message : 'Downloading image' } ) ;
await downloadImage ( app . manifest ) ;
2017-02-17 15:00:55 +01:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 45 , message : 'Ensuring app data directory' } ) ;
await createAppDir ( app ) ;
2017-02-17 15:00:55 +01:00
2021-09-16 13:59:03 -07:00
// re-setup addons since they rely on the app's fqdn (e.g oauth)
await progressCallback ( { percent : 50 , message : 'Setting up addons' } ) ;
await services . setupAddons ( app , app . manifest . addons ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await startApp ( app ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 90 , message : 'Configuring reverse proxy' } ) ;
2021-09-30 09:50:30 -07:00
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2020-11-20 14:13:16 -08:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2015-07-20 00:09:47 -07:00
}
2025-06-11 14:19:31 +02:00
async function updateCommand ( app , args , progressCallback ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 22:08:40 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-01-05 12:14:39 +01:00
2019-09-21 19:45:55 -07:00
const updateConfig = args . updateConfig ;
2015-07-20 00:09:47 -07:00
// app does not want these addons anymore
2016-02-02 08:37:48 -08:00
// FIXME: this does not handle option changes (like multipleDatabases)
2020-11-20 14:13:16 -08:00
const unusedAddons = _ . omit ( app . manifest . addons , Object . keys ( updateConfig . manifest . addons ) ) ;
2021-01-22 11:25:26 +01:00
const httpPortChanged = app . manifest . httpPort !== updateConfig . manifest . httpPort ;
2024-06-06 15:19:19 +02:00
const proxyAuthChanged = ! _ . isEqual ( app . manifest . addons ? . proxyAuth , updateConfig . manifest . addons ? . proxyAuth ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
await progressCallback ( { percent : 5 , message : 'Verify manifest' } ) ;
await verifyManifest ( updateConfig . manifest ) ;
if ( ! updateConfig . skipBackup ) {
await progressCallback ( { percent : 15 , message : 'Backing up app' } ) ;
// preserve update backups for 3 weeks
const [ error ] = await safe ( backuptask . backupApp ( app , { preserveSecs : 3 * 7 * 24 * 60 * 60 } , ( progress ) => {
progressCallback ( { percent : 15 , message : ` Backup - ${ progress . message } ` } ) ;
} ) ) ;
if ( error ) {
error . backupError = true ;
throw error ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
}
2021-08-25 19:41:46 -07:00
2024-04-18 17:35:01 +02:00
await progressCallback ( { percent : 20 , message : 'Updating checklist' } ) ;
2024-09-07 09:37:34 +02:00
await updateChecklist ( app , app . manifest . checklist || { } , true /* new state acked */ ) ;
2024-04-18 17:35:01 +02:00
2021-09-16 13:59:03 -07:00
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
await progressCallback ( { percent : 25 , message : 'Downloading image' } ) ;
await downloadImage ( updateConfig . manifest ) ;
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
2024-08-26 17:26:23 +02:00
await progressCallback ( { percent : 35 , message : 'Deleting old containers' } ) ;
2021-09-16 13:59:03 -07:00
await deleteContainers ( app , { managedOnly : true } ) ;
2024-12-14 14:53:08 +01:00
if ( app . manifest . dockerImage !== updateConfig . manifest . dockerImage ) await docker . deleteImage ( app . manifest . dockerImage ) ;
2021-09-16 13:59:03 -07:00
// only delete unused addons after backup
await services . teardownAddons ( app , unusedAddons ) ;
2024-06-06 15:22:33 +02:00
if ( Object . keys ( unusedAddons ) . includes ( 'localstorage' ) ) await updateApp ( app , { storageVolumeId : null , storageVolumePrefix : null } ) ; // lose reference
2021-09-16 13:59:03 -07:00
2024-07-15 22:59:16 +02:00
// free unused ports. this is done after backup, so the app object is not in some inconsistent state should backup fail
2021-09-16 13:59:03 -07:00
const newTcpPorts = updateConfig . manifest . tcpPorts || { } ;
const newUdpPorts = updateConfig . manifest . udpPorts || { } ;
2024-07-15 22:59:16 +02:00
const portBindings = { ... app . portBindings } ;
2021-09-16 13:59:03 -07:00
2024-07-15 22:59:16 +02:00
for ( const portName of Object . keys ( portBindings ) ) {
2021-09-16 13:59:03 -07:00
if ( newTcpPorts [ portName ] || newUdpPorts [ portName ] ) continue ; // port still in use
2024-07-15 22:59:16 +02:00
delete portBindings [ portName ] ;
}
2021-09-16 13:59:03 -07:00
2024-07-15 22:59:16 +02:00
// clear aliasDomains if needed based multiDomain change
const aliasDomains = app . manifest . multiDomain && ! updateConfig . manifest . multiDomain ? [ ] : app . aliasDomains ;
2021-09-16 13:59:03 -07:00
2024-07-15 22:59:16 +02:00
// clear unused secondaryDomains
const secondaryDomains = [ ... app . secondaryDomains ] ;
const newHttpPorts = updateConfig . manifest . httpPorts || { } ;
for ( let i = secondaryDomains . length - 1 ; i >= 0 ; i -- ) {
const { environmentVariable } = secondaryDomains [ i ] ;
if ( environmentVariable in newHttpPorts ) continue ; // domain still in use
secondaryDomains . splice ( i , 1 ) ; // remove domain
2021-09-16 13:59:03 -07:00
}
2024-07-15 22:59:16 +02:00
const values = {
manifest : updateConfig . manifest ,
portBindings ,
// all domains have to be updated together
subdomain : app . subdomain ,
domain : app . domain ,
aliasDomains ,
secondaryDomains ,
redirectDomains : app . redirectDomains
} ;
2024-07-17 08:58:43 +02:00
if ( 'memoryLimit' in updateConfig ) values . memoryLimit = updateConfig . memoryLimit ;
if ( 'appStoreId' in updateConfig ) values . appStoreId = updateConfig . appStoreId ;
2024-07-15 22:59:16 +02:00
await updateApp ( app , values ) ; // switch over to the new config
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 45 , message : 'Downloading icon' } ) ;
await downloadIcon ( app ) ;
await progressCallback ( { percent : 60 , message : 'Updating addons' } ) ;
await services . setupAddons ( app , updateConfig . manifest . addons ) ;
await progressCallback ( { percent : 70 , message : 'Creating container' } ) ;
await createContainer ( app ) ;
await startApp ( app ) ;
await progressCallback ( { percent : 90 , message : 'Configuring reverse proxy' } ) ;
2021-11-09 21:50:12 -08:00
if ( proxyAuthChanged || httpPortChanged ) {
2021-09-30 09:50:30 -07:00
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2021-09-16 13:59:03 -07:00
}
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null , updateTime : new Date ( ) } ) ;
2015-07-20 00:09:47 -07:00
}
2025-06-11 14:19:31 +02:00
async function startCommand ( app , args , progressCallback ) {
2019-09-22 00:20:12 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof args , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 10 , message : 'Starting app services' } ) ;
await services . startAppServices ( app ) ;
2020-05-18 14:10:00 -07:00
2022-06-09 13:57:57 +02:00
if ( app . manifest . id !== constants . PROXY _APP _APPSTORE _ID ) {
2022-06-08 14:35:50 +02:00
await progressCallback ( { percent : 35 , message : 'Starting container' } ) ;
await docker . startContainer ( app . id ) ;
}
2019-09-22 00:20:12 -07:00
2021-09-16 13:59:03 -07:00
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
await progressCallback ( { percent : 80 , message : 'Configuring reverse proxy' } ) ;
2021-09-30 09:50:30 -07:00
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2020-01-26 16:05:23 -08:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-09-22 00:20:12 -07:00
}
2025-06-11 14:19:31 +02:00
async function stopCommand ( app , args , progressCallback ) {
2019-09-22 00:20:12 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof args , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-06-11 14:19:31 +02:00
// we don't delete the containers. app containers are created with the unless-stopped restart policy. there is no danger of apps getting restarted on reboot
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 20 , message : 'Stopping container' } ) ;
2022-01-28 09:52:03 -08:00
await reverseProxy . unconfigureApp ( app ) ; // removing nginx configs also means that we can auto-cleanup old certs since they are not referenced
2021-09-16 13:59:03 -07:00
await docker . stopContainers ( app . id ) ;
2019-09-22 00:20:12 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 50 , message : 'Stopping app services' } ) ;
await services . stopAppServices ( app ) ;
2020-05-18 14:10:00 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-09-22 00:20:12 -07:00
}
2025-06-11 14:19:31 +02:00
async function restartCommand ( app , args , progressCallback ) {
2019-12-20 10:29:29 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof args , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2022-06-09 13:57:57 +02:00
if ( app . manifest . id !== constants . PROXY _APP _APPSTORE _ID ) {
2022-08-23 11:33:07 +02:00
await progressCallback ( { percent : 10 , message : 'Starting app services' } ) ;
await services . startAppServices ( app ) ;
2022-06-06 20:04:22 +02:00
await progressCallback ( { percent : 20 , message : 'Restarting container' } ) ;
await docker . restartContainer ( app . id ) ;
}
2019-12-20 10:29:29 -08:00
2022-08-23 11:33:07 +02:00
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
await progressCallback ( { percent : 80 , message : 'Configuring reverse proxy' } ) ;
await reverseProxy . configureApp ( app , AuditSource . APPTASK ) ;
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 100 , message : 'Done' } ) ;
await updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
2019-12-20 10:29:29 -08:00
}
2025-06-11 14:19:31 +02:00
async function uninstallCommand ( app , args , progressCallback ) {
2016-01-05 12:14:39 +01:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-21 19:45:55 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 21:56:55 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2016-01-05 12:14:39 +01:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 20 , message : 'Deleting container' } ) ;
await reverseProxy . unconfigureApp ( app ) ;
await deleteContainers ( app , { } ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 30 , message : 'Teardown addons' } ) ;
await services . teardownAddons ( app , app . manifest . addons ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 40 , message : 'Cleanup file manager' } ) ;
2020-07-29 15:02:30 +02:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 50 , message : 'Deleting app data directory' } ) ;
await deleteAppDir ( app , { removeDirectory : true } ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 60 , message : 'Deleting image' } ) ;
2024-12-14 14:53:08 +01:00
await docker . deleteImage ( app . manifest . dockerImage ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 70 , message : 'Unregistering domains' } ) ;
2022-01-16 12:32:12 -08:00
await dns . unregisterLocations ( [ { subdomain : app . subdomain , domain : app . domain } ] . concat ( app . secondaryDomains ) . concat ( app . redirectDomains ) . concat ( app . aliasDomains ) , progressCallback ) ;
2015-07-20 00:09:47 -07:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 90 , message : 'Cleanup logs' } ) ;
await cleanupLogs ( app ) ;
2018-06-04 11:45:22 +02:00
2021-09-16 13:59:03 -07:00
await progressCallback ( { percent : 95 , message : 'Remove app from database' } ) ;
await apps . del ( app . id ) ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
async function run ( appId , args , progressCallback ) {
2019-08-26 15:55:57 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2019-08-27 15:18:16 -07:00
assert . strictEqual ( typeof args , 'object' ) ;
2019-08-26 15:55:57 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-09-16 13:59:03 -07:00
const app = await apps . get ( appId ) ;
2021-09-25 21:38:09 -07:00
debug ( ` run: startTask installationState: ${ app . installationState } runState: ${ app . runState } ` ) ;
2021-09-16 13:59:03 -07:00
let cmd ;
switch ( app . installationState ) {
case apps . ISTATE _PENDING _INSTALL :
case apps . ISTATE _PENDING _CLONE :
case apps . ISTATE _PENDING _RESTORE :
case apps . ISTATE _PENDING _IMPORT :
2025-06-11 14:19:31 +02:00
cmd = installCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _CONFIGURE :
2025-06-11 14:19:31 +02:00
cmd = configureCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
2025-06-11 14:19:31 +02:00
case apps . ISTATE _PENDING _RECREATE _CONTAINER : // mounts/devices/env
case apps . ISTATE _PENDING _RESIZE : // memory/cpu
2021-09-16 13:59:03 -07:00
case apps . ISTATE _PENDING _DEBUG :
2025-06-11 14:19:31 +02:00
cmd = createCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _LOCATION _CHANGE :
2025-06-11 14:19:31 +02:00
cmd = changeLocationCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
2023-07-13 15:06:07 +05:30
case apps . ISTATE _PENDING _SERVICES _CHANGE :
2025-06-11 14:19:31 +02:00
cmd = changeServicesCommand ( app , args , progressCallback ) ;
2023-07-13 15:06:07 +05:30
break ;
2021-09-16 13:59:03 -07:00
case apps . ISTATE _PENDING _DATA _DIR _MIGRATION :
2025-06-11 14:19:31 +02:00
cmd = migrateDataDirCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _UNINSTALL :
2025-06-11 14:19:31 +02:00
cmd = uninstallCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _UPDATE :
2025-06-11 14:19:31 +02:00
cmd = updateCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _BACKUP :
2025-06-11 14:19:31 +02:00
cmd = backupCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _START :
2025-06-11 14:19:31 +02:00
cmd = startCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _STOP :
2025-06-11 14:19:31 +02:00
cmd = stopCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _PENDING _RESTART :
2025-06-11 14:19:31 +02:00
cmd = restartCommand ( app , args , progressCallback ) ;
2021-09-16 13:59:03 -07:00
break ;
case apps . ISTATE _INSTALLED : // can only happen when we have a bug in our code while testing/development
cmd = updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } ) ;
break ;
default :
2021-09-25 21:38:09 -07:00
debug ( 'run: apptask launched with invalid command' ) ;
2021-09-16 13:59:03 -07:00
throw new BoxError ( BoxError . INTERNAL _ERROR , 'Unknown install command in apptask:' + app . installationState ) ;
}
2021-09-30 10:45:25 -07:00
const [ error , result ] = await safe ( cmd ) ; // only some commands like backup return a result
2021-09-16 13:59:03 -07:00
if ( error ) {
2023-04-16 10:49:59 +02:00
debug ( ` run: app error for state ${ app . installationState } : %o ` , error ) ;
2021-09-16 13:59:03 -07:00
if ( app . installationState === apps . ISTATE _PENDING _BACKUP ) {
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
await safe ( updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null } ) , { debug } ) ;
} else if ( app . installationState === apps . ISTATE _PENDING _UPDATE && error . backupError ) {
2021-09-25 21:38:09 -07:00
debug ( 'run: update aborted because backup failed' ) ;
2021-09-16 13:59:03 -07:00
await safe ( updateApp ( app , { installationState : apps . ISTATE _INSTALLED , error : null , health : null } , { debug } ) ) ;
} else {
await safe ( updateApp ( app , { installationState : apps . ISTATE _ERROR , error : makeTaskError ( error , app ) } ) , { debug } ) ;
2015-07-20 00:09:47 -07:00
}
2021-09-16 13:59:03 -07:00
throw error ;
}
2021-09-30 10:45:25 -07:00
return result || null ;
2015-07-20 00:09:47 -07:00
}