2016-05-24 09:40:26 -07:00
'use strict' ;
exports = module . exports = {
2023-08-12 19:28:07 +05:30
initialize ,
uninitialize ,
onActivated ,
2023-08-13 10:29:24 +05:30
onDashboardLocationChanged ,
2018-01-26 22:35:08 -08:00
2022-11-30 19:54:32 +01:00
getStatus
2016-05-24 09:40:26 -07:00
} ;
2021-01-21 11:31:35 -08:00
const apps = require ( './apps.js' ) ,
2016-05-24 10:58:18 -07:00
assert = require ( 'assert' ) ,
2021-11-17 10:33:28 -08:00
AuditSource = require ( './auditsource.js' ) ,
2021-11-02 22:30:38 -07:00
BoxError = require ( './boxerror.js' ) ,
2023-02-21 12:03:58 +01:00
constants = require ( './constants.js' ) ,
2023-08-12 19:28:07 +05:30
cron = require ( './cron.js' ) ,
dashboard = require ( './dashboard.js' ) ,
database = require ( './database.js' ) ,
2016-05-24 09:40:26 -07:00
debug = require ( 'debug' ) ( 'box:platform' ) ,
2023-08-08 10:42:16 +05:30
docker = require ( './docker.js' ) ,
2023-08-12 19:28:07 +05:30
dockerProxy = require ( './dockerproxy.js' ) ,
2016-05-24 10:52:55 -07:00
fs = require ( 'fs' ) ,
2016-05-24 13:10:18 -07:00
infra = require ( './infra_version.js' ) ,
2017-11-28 23:18:43 -08:00
locker = require ( './locker.js' ) ,
2023-08-12 19:28:07 +05:30
oidc = require ( './oidc.js' ) ,
2016-05-24 09:40:26 -07:00
paths = require ( './paths.js' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2016-05-24 13:10:18 -07:00
safe = require ( 'safetydance' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2016-05-24 13:16:31 -07:00
shell = require ( './shell.js' ) ,
2019-08-28 15:00:55 -07:00
tasks = require ( './tasks.js' ) ,
2023-05-14 10:53:50 +02:00
timers = require ( 'timers/promises' ) ,
2023-08-12 19:28:07 +05:30
updater = require ( './updater.js' ) ,
users = require ( './users.js' ) ,
2021-09-28 11:51:01 -07:00
volumes = require ( './volumes.js' ) ,
2016-07-24 23:19:11 -07:00
_ = require ( 'underscore' ) ;
2016-05-24 09:40:26 -07:00
2022-11-30 19:54:32 +01:00
let gStatusMessage = 'Initializing' ;
function getStatus ( ) {
return { message : gStatusMessage } ;
}
2021-09-07 09:57:49 -07:00
async function pruneInfraImages ( ) {
2018-10-27 13:04:13 -07:00
debug ( 'pruneInfraImages: checking existing images' ) ;
2022-11-30 19:57:25 +01:00
// cannot blindly remove all unused images since redis image may not be used
2023-08-08 10:42:16 +05:30
const imageNames = Object . keys ( infra . images ) . map ( addon => infra . images [ addon ] ) ;
const output = safe . child _process . execSync ( 'docker images --digests --format "{{.ID}} {{.Repository}} {{.Tag}} {{.Digest}}"' , { encoding : 'utf8' } ) ;
if ( output === null ) {
debug ( ` Failed to list images ${ safe . error . message } ` ) ;
throw safe . error ;
}
const lines = output . trim ( ) . split ( '\n' ) ;
2022-11-30 19:57:25 +01:00
2023-08-08 10:42:16 +05:30
for ( const imageName of imageNames ) {
const parsedTag = docker . parseImageName ( imageName ) ;
2022-11-30 19:57:25 +01:00
2022-11-30 20:59:14 +01:00
for ( const line of lines ) {
2022-11-30 19:57:25 +01:00
if ( ! line ) continue ;
2023-08-08 10:42:16 +05:30
const [ , repo , tag , digest ] = line . split ( ' ' ) ; // [ ID, Repo, Tag, Digest ]
if ( ! parsedTag . repository . endsWith ( repo ) ) continue ; // some other repo
if ( imageName === ` ${ repo } : ${ tag } @ ${ digest } ` ) continue ; // the image we want to keep
2022-11-30 20:59:14 +01:00
2023-08-08 10:42:16 +05:30
const imageIdToPrune = tag === '<none>' ? ` ${ repo } @ ${ digest } ` : ` ${ repo } : ${ tag } ` ; // untagged, use digest
console . log ( ` pruneInfraImages: removing unused image of ${ imageName } : ${ imageIdToPrune } ` ) ;
2022-11-30 19:57:25 +01:00
2023-08-08 10:42:16 +05:30
const result = safe . child _process . execSync ( ` docker rmi ' ${ imageIdToPrune } ' ` , { encoding : 'utf8' } ) ;
if ( result === null ) console . log ( ` Error removing image ${ imageIdToPrune } : ${ safe . error . mesage } ` ) ;
2022-11-30 19:57:25 +01:00
}
}
2016-05-24 13:16:31 -07:00
}
2022-02-09 17:28:46 -08:00
async function createDockerNetwork ( ) {
debug ( 'createDockerNetwork: recreating docker network' ) ;
2022-02-09 17:47:48 -08:00
await shell . promises . exec ( 'createDockerNetwork' , 'docker network rm cloudron || true' ) ;
// the --ipv6 option will work even in ipv6 is disabled. fd00 is IPv6 ULA
2023-02-21 12:03:58 +01:00
await shell . promises . exec ( 'createDockerNetwork' , ` docker network create --subnet= ${ constants . DOCKER _IPv4 _SUBNET } --ip-range= ${ constants . DOCKER _IPv4 _RANGE } --gateway ${ constants . DOCKER _IPv4 _GATEWAY } --ipv6 --subnet=fd00:c107:d509::/64 cloudron ` ) ;
2022-02-09 17:28:46 -08:00
}
2021-09-07 09:57:49 -07:00
async function removeAllContainers ( ) {
2020-07-30 14:36:11 -07:00
debug ( 'removeAllContainers: removing all containers for infra upgrade' ) ;
2016-07-25 00:39:57 -07:00
2021-09-07 09:57:49 -07:00
await shell . promises . exec ( 'removeAllContainers' , 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop' ) ;
await shell . promises . exec ( 'removeAllContainers' , 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f' ) ;
2016-05-24 10:52:55 -07:00
}
2016-05-24 10:58:18 -07:00
2023-08-12 19:28:07 +05:30
async function markApps ( existingInfra , restoreOptions ) {
2021-02-24 14:56:09 -08:00
assert . strictEqual ( typeof existingInfra , 'object' ) ;
2023-08-12 19:28:07 +05:30
assert . strictEqual ( typeof restoreOptions , 'object' ) ;
2021-02-24 14:56:09 -08:00
2018-11-10 18:21:15 -08:00
if ( existingInfra . version === 'none' ) { // cloudron is being restored from backup
2020-07-30 14:09:25 -07:00
debug ( 'markApps: restoring installed apps' ) ;
2023-08-12 19:28:07 +05:30
await apps . restoreInstalledApps ( restoreOptions , AuditSource . PLATFORM ) ;
2018-10-16 11:04:34 -07:00
} else if ( existingInfra . version !== infra . version ) {
2020-07-30 14:09:25 -07:00
debug ( 'markApps: reconfiguring installed apps' ) ;
2018-01-30 12:23:27 -08:00
reverseProxy . removeAppConfigs ( ) ; // should we change the cert location, nginx will not start
2023-07-21 17:10:25 +02:00
await apps . configureInstalledApps ( await apps . list ( ) , AuditSource . PLATFORM ) ;
2018-10-16 11:04:34 -07:00
} else {
2020-05-22 16:43:16 -07:00
let changedAddons = [ ] ;
2023-08-08 10:42:16 +05:30
if ( infra . images . mysql !== existingInfra . images . mysql ) changedAddons . push ( 'mysql' ) ;
if ( infra . images . postgresql !== existingInfra . images . postgresql ) changedAddons . push ( 'postgresql' ) ;
if ( infra . images . mongodb !== existingInfra . images . mongodb ) changedAddons . push ( 'mongodb' ) ;
if ( infra . images . redis !== existingInfra . images . redis ) changedAddons . push ( 'redis' ) ;
2020-05-22 16:43:16 -07:00
if ( changedAddons . length ) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
2020-07-30 14:09:25 -07:00
debug ( ` markApps: changedAddons: ${ JSON . stringify ( changedAddons ) } ` ) ;
2021-11-17 10:33:28 -08:00
await apps . restartAppsUsingAddons ( changedAddons , AuditSource . PLATFORM ) ;
2020-05-22 16:43:16 -07:00
} else {
2020-07-30 14:09:25 -07:00
debug ( 'markApps: apps are already uptodate' ) ;
2020-05-22 16:43:16 -07:00
}
2016-07-25 18:57:54 -07:00
}
}
2023-08-12 19:28:07 +05:30
async function onInfraReady ( infraChanged ) {
debug ( ` onInfraReady: platform is ready. infra changed: ${ infraChanged } ` ) ;
gStatusMessage = 'Ready' ;
if ( infraChanged ) await safe ( pruneInfraImages ( ) , { debug } ) ; // ignore error
await apps . schedulePendingTasks ( AuditSource . PLATFORM ) ;
}
async function startInfra ( restoreOptions ) {
assert . strictEqual ( typeof restoreOptions , 'object' ) ;
if ( process . env . BOX _ENV === 'test' && ! process . env . TEST _CREATE _INFRA ) return ;
debug ( 'startInfra: checking infrastructure' ) ;
let existingInfra = { version : 'none' } ;
if ( fs . existsSync ( paths . INFRA _VERSION _FILE ) ) {
existingInfra = safe . JSON . parse ( fs . readFileSync ( paths . INFRA _VERSION _FILE , 'utf8' ) ) ;
if ( ! existingInfra ) existingInfra = { version : 'corrupt' } ;
}
// short-circuit for the restart case
if ( _ . isEqual ( infra , existingInfra ) ) {
debug ( 'startInfra: infra is uptodate at version %s' , infra . version ) ;
await onInfraReady ( false /* !infraChanged */ ) ;
return ;
}
debug ( ` startInfra: updating infrastructure from ${ existingInfra . version } to ${ infra . version } ` ) ;
const error = locker . lock ( locker . OP _INFRA _START ) ;
if ( error ) throw error ;
for ( let attempt = 0 ; attempt < 5 ; attempt ++ ) {
try {
if ( existingInfra . version !== infra . version ) {
gStatusMessage = 'Removing containers for upgrade' ;
await removeAllContainers ( ) ;
await createDockerNetwork ( ) ;
}
if ( existingInfra . version === 'none' ) await volumes . mountAll ( ) ; // when restoring, mount all volumes
await markApps ( existingInfra , restoreOptions ) ; // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
gStatusMessage = 'Starting services, this can take a while' ;
await services . startServices ( existingInfra ) ;
await fs . promises . writeFile ( paths . INFRA _VERSION _FILE , JSON . stringify ( infra , null , 4 ) ) ;
break ;
} catch ( error ) {
// for some reason, mysql arbitrary restarts making startup tasks fail. this makes the box update stuck
// LOST is when existing connection breaks. REFUSED is when new connection cannot connect at all
const retry = error . reason === BoxError . DATABASE _ERROR && ( error . code === 'PROTOCOL_CONNECTION_LOST' || error . code === 'ECONNREFUSED' ) ;
debug ( ` startInfra: Failed to start services. retry= ${ retry } (attempt ${ attempt } ): ${ error . message } ` ) ;
if ( ! retry ) throw error ; // refuse to start
await timers . setTimeout ( 10000 ) ;
}
}
locker . unlock ( locker . OP _INFRA _START ) ;
await onInfraReady ( true /* infraChanged */ ) ;
}
async function initialize ( ) {
debug ( 'initializing platform' ) ;
await database . initialize ( ) ;
await tasks . stopAllTasks ( ) ;
// always generate webadmin config since we have no versioning mechanism for the ejs
const { domain : dashboardDomain } = await dashboard . getLocation ( ) ;
if ( dashboardDomain ) await safe ( reverseProxy . writeDashboardConfig ( dashboardDomain ) , { debug } ) ; // ok to fail if no disk space
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
// we remove the config as a simple security measure to not expose IP <-> domain
const activated = await users . isActivated ( ) ;
if ( ! activated ) {
debug ( 'start: not activated. generating IP based redirection config' ) ;
await safe ( reverseProxy . writeDefaultConfig ( { activated : false } ) , { debug } ) ; // ok to fail if no disk space
}
await updater . notifyUpdate ( ) ;
if ( await users . isActivated ( ) ) safe ( onActivated ( { skipDnsSetup : false } ) , { debug } ) ; // run in background
}
async function uninitialize ( ) {
debug ( 'uninitializing platform' ) ;
await cron . stopJobs ( ) ;
await dockerProxy . stop ( ) ;
await tasks . stopAllTasks ( ) ;
await database . uninitialize ( ) ;
}
async function onActivated ( restoreOptions ) {
assert . strictEqual ( typeof restoreOptions , 'object' ) ;
debug ( 'onActivated: starting post activation services' ) ;
// Starting the infra after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
await startInfra ( restoreOptions ) ;
await cron . startJobs ( ) ;
await dockerProxy . start ( ) ; // this relies on the 'cloudron' docker network interface to be available
await oidc . start ( ) ; // this requires dashboardFqdn to be set
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
// the UI some time to query the dashboard domain in the restore code path
await timers . setTimeout ( 30000 ) ;
await reverseProxy . writeDefaultConfig ( { activated : true } ) ;
}
2023-08-13 10:29:24 +05:30
async function onDashboardLocationChanged ( auditSource ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
// mark apps using oidc addon to be reconfigured
const [ , installedApps ] = await safe ( apps . list ( ) ) ;
await safe ( apps . configureInstalledApps ( installedApps . filter ( ( a ) => ! ! a . manifest . addons . oidc ) , auditSource ) ) ;
await safe ( services . rebuildService ( 'turn' , auditSource ) , { debug } ) ; // to update the realm variable
await oidc . stop ( ) ;
await oidc . start ( ) ;
}