2015-07-20 00:09:47 -07:00
'use strict' ;
2015-10-19 11:24:21 -07:00
exports = module . exports = {
2020-11-18 23:24:34 -08:00
testRegistryConfig ,
setRegistryConfig ,
injectPrivateFields ,
removePrivateFields ,
ping ,
info ,
downloadImage ,
createContainer ,
startContainer ,
restartContainer ,
stopContainer ,
2015-12-23 13:23:47 -08:00
stopContainerByName : stopContainer ,
2020-11-18 23:24:34 -08:00
stopContainers ,
deleteContainer ,
deleteImage ,
deleteContainers ,
createSubcontainer ,
getContainerIdByIp ,
inspect ,
getContainerIp ,
2018-02-27 13:21:38 -08:00
inspectByName : inspect ,
2020-11-18 23:24:34 -08:00
execContainer ,
getEvents ,
memoryUsage ,
createVolume ,
removeVolume ,
2021-01-20 12:01:15 -08:00
clearVolume ,
update
2015-10-19 11:24:21 -07:00
} ;
2015-10-19 11:08:23 -07:00
2021-01-21 11:31:35 -08:00
const apps = require ( './apps.js' ) ,
2016-04-18 16:30:58 -07:00
async = require ( 'async' ) ,
assert = require ( 'assert' ) ,
2019-09-23 12:13:21 -07:00
BoxError = require ( './boxerror.js' ) ,
2016-04-18 16:30:58 -07:00
child _process = require ( 'child_process' ) ,
constants = require ( './constants.js' ) ,
2019-12-06 13:52:43 -08:00
debug = require ( 'debug' ) ( 'box:docker' ) ,
2019-12-04 13:17:58 -08:00
Docker = require ( 'dockerode' ) ,
2020-11-18 11:43:28 -08:00
os = require ( 'os' ) ,
2018-09-13 13:55:49 -07:00
path = require ( 'path' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2018-04-12 11:32:49 -07:00
shell = require ( './shell.js' ) ,
2019-08-06 09:45:16 -07:00
safe = require ( 'safetydance' ) ,
2021-01-20 11:45:04 -08:00
system = require ( './system.js' ) ,
2016-04-18 16:30:58 -07:00
util = require ( 'util' ) ,
2020-10-27 22:39:05 -07:00
volumes = require ( './volumes.js' ) ,
2020-04-27 22:55:43 -07:00
_ = require ( 'underscore' ) ;
2016-04-18 16:30:58 -07:00
2019-01-18 14:48:31 -08:00
const CLEARVOLUME _CMD = path . join ( _ _dirname , 'scripts/clearvolume.sh' ) ,
2018-12-20 14:33:29 -08:00
MKDIRVOLUME _CMD = path . join ( _ _dirname , 'scripts/mkdirvolume.sh' ) ;
2018-09-13 13:55:49 -07:00
2019-12-04 14:37:00 -08:00
const DOCKER _SOCKET _PATH = '/var/run/docker.sock' ;
const gConnection = new Docker ( { socketPath : DOCKER _SOCKET _PATH } ) ;
2019-12-04 13:17:58 -08:00
2019-10-22 22:07:44 -07:00
function testRegistryConfig ( auth , callback ) {
assert . strictEqual ( typeof auth , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
gConnection . checkAuth ( auth , function ( error /*, data */ ) { // this returns a 500 even for auth errors
2019-10-23 06:11:17 -07:00
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , error , { field : 'serverAddress' } ) ) ;
callback ( ) ;
} ) ;
2019-10-22 22:07:44 -07:00
}
function injectPrivateFields ( newConfig , currentConfig ) {
2020-05-14 23:01:44 +02:00
if ( newConfig . password === constants . SECRET _PLACEHOLDER ) newConfig . password = currentConfig . password ;
2019-10-22 22:07:44 -07:00
}
function removePrivateFields ( registryConfig ) {
assert . strictEqual ( typeof registryConfig , 'object' ) ;
2020-05-14 23:01:44 +02:00
if ( registryConfig . password ) registryConfig . password = constants . SECRET _PLACEHOLDER ;
2019-10-22 22:07:44 -07:00
return registryConfig ;
}
2018-10-12 17:04:04 -07:00
function setRegistryConfig ( auth , callback ) {
assert . strictEqual ( typeof auth , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const isLogin = ! ! auth . password ;
// currently, auth info is not stashed in the db but maybe it should for restore to work?
2019-10-22 22:07:44 -07:00
const cmd = isLogin ? ` docker login ${ auth . serverAddress } --username ${ auth . username } --password ${ auth . password } ` : ` docker logout ${ auth . serverAddress } ` ;
2018-10-12 17:04:04 -07:00
2019-09-23 12:13:21 -07:00
child _process . exec ( cmd , { } , function ( error /*, stdout, stderr */ ) {
if ( error ) return callback ( new BoxError ( BoxError . ACCESS _DENIED , error . message ) ) ;
2018-10-12 17:04:04 -07:00
callback ( ) ;
} ) ;
}
2018-11-19 10:19:46 +01:00
function ping ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-23 15:49:47 +01:00
// do not let the request linger
2019-12-04 14:37:00 -08:00
const connection = new Docker ( { socketPath : DOCKER _SOCKET _PATH , timeout : 1000 } ) ;
2018-11-19 10:19:46 +01:00
2019-12-04 13:17:58 -08:00
connection . ping ( function ( error , result ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2020-01-30 16:06:11 -08:00
if ( Buffer . isBuffer ( result ) && result . toString ( 'utf8' ) === 'OK' ) return callback ( null ) ; // sometimes it returns buffer
if ( result === 'OK' ) return callback ( null ) ;
2018-11-19 10:19:46 +01:00
2020-01-30 16:06:11 -08:00
callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Unable to ping the docker daemon' ) ) ;
2018-11-19 10:19:46 +01:00
} ) ;
}
2019-10-27 12:14:27 -07:00
function getRegistryConfig ( image , callback ) {
2019-11-17 11:50:55 -08:00
// https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L62
2019-10-27 12:14:27 -07:00
const parts = image . split ( '/' ) ;
2019-11-17 11:50:55 -08:00
if ( parts . length === 1 || ( parts [ 0 ] . match ( /[.:]/ ) === null ) ) return callback ( null , null ) ; // public docker registry
2019-10-27 12:14:27 -07:00
settings . getRegistryConfig ( function ( error , registryConfig ) {
if ( error ) return callback ( error ) ;
// https://github.com/apocas/dockerode#pull-from-private-repos
const auth = {
username : registryConfig . username ,
password : registryConfig . password ,
auth : registryConfig . auth || '' , // the auth token at login time
email : registryConfig . email || '' ,
serveraddress : registryConfig . serverAddress
} ;
callback ( null , auth ) ;
} ) ;
}
2015-10-19 15:37:57 -07:00
function pullImage ( manifest , callback ) {
2019-10-27 12:14:27 -07:00
getRegistryConfig ( manifest . dockerImage , function ( error , authConfig ) {
if ( error ) return callback ( error ) ;
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
debug ( ` pullImage: will pull ${ manifest . dockerImage } . auth: ${ authConfig ? 'yes' : 'no' } ` ) ;
2015-10-19 11:40:19 -07:00
2019-12-04 13:17:58 -08:00
gConnection . pull ( manifest . dockerImage , { authconfig : authConfig } , function ( error , stream ) {
2020-03-06 16:39:20 -08:00
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND , ` Unable to pull image ${ manifest . dockerImage } . message: ${ error . message } statusCode: ${ error . statusCode } ` ) ) ;
2020-02-10 21:54:08 -08:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , ` Unable to pull image ${ manifest . dockerImage } . Please check the network or if the image needs authentication. statusCode: ${ error . statusCode } ` ) ) ;
2019-10-27 12:14:27 -07:00
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream . on ( 'data' , function ( chunk ) {
var data = safe . JSON . parse ( chunk ) || { } ;
2019-11-15 17:28:43 -08:00
debug ( 'pullImage: %j' , data ) ;
2019-10-27 12:14:27 -07:00
// The data.status here is useless because this is per layer as opposed to per image
if ( ! data . status && data . error ) {
2019-11-15 17:28:43 -08:00
debug ( 'pullImage error %s: %s' , manifest . dockerImage , data . errorDetail . message ) ;
2019-10-27 12:14:27 -07:00
}
} ) ;
stream . on ( 'end' , function ( ) {
2019-11-15 17:28:43 -08:00
debug ( 'downloaded image %s' , manifest . dockerImage ) ;
2019-10-27 12:14:27 -07:00
callback ( null ) ;
} ) ;
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
stream . on ( 'error' , function ( error ) {
2019-11-15 17:28:43 -08:00
debug ( 'error pulling image %s: %j' , manifest . dockerImage , error ) ;
2015-10-19 11:40:19 -07:00
2019-10-27 12:14:27 -07:00
callback ( new BoxError ( BoxError . DOCKER _ERROR , error . message ) ) ;
} ) ;
2015-10-19 11:40:19 -07:00
} ) ;
} ) ;
}
2015-10-19 15:37:57 -07:00
function downloadImage ( manifest , callback ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-11-15 17:28:43 -08:00
debug ( 'downloadImage %s' , manifest . dockerImage ) ;
2015-10-19 11:40:19 -07:00
2020-10-19 22:11:23 -07:00
const image = gConnection . getImage ( manifest . dockerImage ) ;
2015-10-19 11:40:19 -07:00
2020-10-19 22:11:23 -07:00
image . inspect ( function ( error , result ) {
if ( ! error && result ) return callback ( null ) ; // image is already present locally
2015-10-19 11:40:19 -07:00
2020-10-19 22:11:23 -07:00
let attempt = 1 ;
async . retry ( { times : 10 , interval : 5000 , errorFilter : e => e . reason !== BoxError . NOT _FOUND } , function ( retryCallback ) {
debug ( 'Downloading image %s. attempt: %s' , manifest . dockerImage , attempt ++ ) ;
pullImage ( manifest , retryCallback ) ;
} , callback ) ;
} ) ;
2015-10-19 11:40:19 -07:00
}
2020-10-27 22:39:05 -07:00
function getBinds ( app , callback ) {
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-10-29 11:47:37 -07:00
if ( app . mounts . length === 0 ) return callback ( null ) ;
2020-04-29 21:55:21 -07:00
let binds = [ ] ;
2020-10-27 22:39:05 -07:00
volumes . list ( function ( error , result ) {
if ( error ) return callback ( error ) ;
2020-10-29 11:47:37 -07:00
let volumesById = { } ;
result . forEach ( r => volumesById [ r . id ] = r ) ;
2020-10-27 22:39:05 -07:00
2020-10-29 11:47:37 -07:00
for ( const mount of app . mounts ) {
const volume = volumesById [ mount . volumeId ] ;
binds . push ( ` ${ volume . hostPath } :/media/ ${ volume . name } : ${ mount . readOnly ? 'ro' : 'rw' } ` ) ;
2020-10-27 22:39:05 -07:00
}
2020-04-29 21:55:21 -07:00
2020-10-29 11:47:37 -07:00
callback ( null , binds ) ;
} ) ;
2020-04-29 21:55:21 -07:00
}
2020-11-18 11:43:28 -08:00
function getLowerUpIp ( ) { // see getifaddrs and IFF_LOWER_UP and netdevice
const ni = os . networkInterfaces ( ) ; // { lo: [], eth0: [] }
for ( const iname of Object . keys ( ni ) ) {
if ( iname === 'lo' ) continue ;
for ( const address of ni [ iname ] ) {
if ( ! address . internal && address . family === 'IPv4' ) return address . address ;
}
}
return null ;
}
2020-04-27 22:55:43 -07:00
function createSubcontainer ( app , name , cmd , options , callback ) {
2015-10-19 11:40:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2015-11-02 09:34:31 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2015-10-19 16:22:35 -07:00
assert ( ! cmd || util . isArray ( cmd ) ) ;
2020-04-27 22:55:43 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-10-19 11:40:19 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-08-18 22:35:08 -07:00
let isAppContainer = ! cmd ; // non app-containers are like scheduler
2015-10-19 11:40:19 -07:00
var manifest = app . manifest ;
var exposedPorts = { } , dockerPortBindings = { } ;
2018-02-08 15:07:49 +01:00
var domain = app . fqdn ;
2019-06-03 13:45:03 -07:00
const envPrefix = manifest . manifestVersion <= 1 ? '' : 'CLOUDRON_' ;
let stdEnv = [
2015-10-19 11:40:19 -07:00
'CLOUDRON=1' ,
2018-11-22 16:50:02 -08:00
'CLOUDRON_PROXY_IP=172.18.0.1' ,
2019-06-26 14:18:39 -07:00
` CLOUDRON_APP_HOSTNAME= ${ app . id } ` ,
2019-07-26 10:49:29 -07:00
` ${ envPrefix } WEBADMIN_ORIGIN= ${ settings . adminOrigin ( ) } ` ,
` ${ envPrefix } API_ORIGIN= ${ settings . adminOrigin ( ) } ` ,
2019-06-03 13:45:03 -07:00
` ${ envPrefix } APP_ORIGIN=https:// ${ domain } ` ,
` ${ envPrefix } APP_DOMAIN= ${ domain } `
2015-10-19 11:40:19 -07:00
] ;
2015-10-19 16:00:40 -07:00
var portEnv = [ ] ;
2018-08-12 19:33:11 -07:00
for ( let portName in app . portBindings ) {
2018-08-12 22:47:59 -07:00
const hostPort = app . portBindings [ portName ] ;
2019-10-11 20:35:59 -07:00
const portType = ( manifest . tcpPorts && portName in manifest . tcpPorts ) ? 'tcp' : 'udp' ;
2018-08-12 22:47:59 -07:00
const ports = portType == 'tcp' ? manifest . tcpPorts : manifest . udpPorts ;
2015-10-19 11:40:19 -07:00
2018-08-12 22:47:59 -07:00
var containerPort = ports [ portName ] . containerPort || hostPort ;
2020-11-18 23:24:34 -08:00
// docker portBindings requires ports to be exposed
2018-08-12 22:47:59 -07:00
exposedPorts [ ` ${ containerPort } / ${ portType } ` ] = { } ;
2018-08-12 19:33:11 -07:00
portEnv . push ( ` ${ portName } = ${ hostPort } ` ) ;
2015-10-19 11:40:19 -07:00
2020-11-18 11:43:28 -08:00
const hostIp = hostPort === 53 ? getLowerUpIp ( ) : '0.0.0.0' ; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings [ ` ${ containerPort } / ${ portType } ` ] = [ { HostIp : hostIp , HostPort : hostPort + '' } ] ;
2015-10-19 11:40:19 -07:00
}
2018-10-11 16:18:38 -07:00
2018-10-11 14:07:43 -07:00
let appEnv = [ ] ;
Object . keys ( app . env ) . forEach ( function ( name ) { appEnv . push ( ` ${ name } = ${ app . env [ name ] } ` ) ; } ) ;
2015-10-19 11:40:19 -07:00
2021-01-20 12:12:14 -08:00
let memoryLimit = apps . getMemoryLimit ( app ) ;
2016-02-11 17:00:21 +01:00
2018-02-27 10:51:21 -08:00
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if ( ! isAppContainer ) memoryLimit *= 2 ;
2021-01-21 11:31:35 -08:00
services . getEnvironment ( app , function ( error , addonEnv ) {
2019-10-24 20:31:45 -07:00
if ( error ) return callback ( error ) ;
2015-10-19 16:00:40 -07:00
2020-10-27 22:39:05 -07:00
getBinds ( app , function ( error , binds ) {
if ( error ) return callback ( error ) ;
let containerOptions = {
name : name , // for referencing containers
Tty : isAppContainer ,
Image : app . manifest . dockerImage ,
Cmd : ( isAppContainer && app . debugMode && app . debugMode . cmd ) ? app . debugMode . cmd : cmd ,
Env : stdEnv . concat ( addonEnv ) . concat ( portEnv ) . concat ( appEnv ) ,
ExposedPorts : isAppContainer ? exposedPorts : { } ,
Volumes : { // see also ReadonlyRootfs
'/tmp' : { } ,
'/run' : { }
2018-01-16 17:41:18 +01:00
} ,
2020-10-27 22:39:05 -07:00
Labels : {
'fqdn' : app . fqdn ,
'appId' : app . id ,
'isSubcontainer' : String ( ! isAppContainer ) ,
'isCloudronManaged' : String ( true )
2015-10-19 16:00:40 -07:00
} ,
2020-10-27 22:39:05 -07:00
HostConfig : {
2021-01-21 11:31:35 -08:00
Mounts : services . getMountsSync ( app , app . manifest . addons ) ,
2020-10-27 22:39:05 -07:00
Binds : binds , // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig : {
Type : 'syslog' ,
Config : {
'tag' : app . id ,
'syslog-address' : 'udp://127.0.0.1:2514' , // see apps.js:validatePortBindings()
'syslog-format' : 'rfc5424'
}
} ,
2021-01-20 11:45:04 -08:00
Memory : system . getMemoryAllocation ( memoryLimit ) ,
2020-10-27 22:39:05 -07:00
MemorySwap : memoryLimit , // Memory + Swap
PortBindings : isAppContainer ? dockerPortBindings : { } ,
PublishAllPorts : false ,
ReadonlyRootfs : app . debugMode ? ! ! app . debugMode . readonlyRootfs : true ,
RestartPolicy : {
'Name' : isAppContainer ? 'unless-stopped' : 'no' ,
'MaximumRetryCount' : 0
} ,
CpuShares : app . cpuShares ,
VolumesFrom : isAppContainer ? null : [ app . containerId + ':rw' ] ,
SecurityOpt : [ 'apparmor=docker-cloudron-app' ] ,
CapAdd : [ ] ,
CapDrop : [ ]
2019-06-01 10:48:17 -07:00
}
2020-08-18 22:35:08 -07:00
} ;
2017-08-11 23:22:48 +01:00
2020-10-27 22:39:05 -07:00
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if ( isAppContainer ) {
containerOptions . Hostname = app . id ;
containerOptions . HostConfig . NetworkMode = 'cloudron' ; // user defined bridge network
2020-11-25 12:04:56 -08:00
containerOptions . HostConfig . Dns = [ '172.18.0.1' ] ; // use internal dns
containerOptions . HostConfig . DnsSearch = [ '.' ] ; // use internal dns
2020-10-27 22:39:05 -07:00
containerOptions . NetworkingConfig = {
EndpointsConfig : {
cloudron : {
2020-11-20 14:13:16 -08:00
IPAMConfig : {
IPv4Address : app . containerIp
} ,
2020-10-27 22:39:05 -07:00
Aliases : [ name ] // adds hostname entry with container name
}
}
} ;
} else {
2020-12-03 11:48:25 -08:00
containerOptions . HostConfig . NetworkMode = ` container: ${ app . containerId } ` ; // scheduler containers must have same IP as app for various addon auth
2020-10-27 22:39:05 -07:00
}
var capabilities = manifest . capabilities || [ ] ;
2020-06-30 07:31:24 -07:00
2020-10-27 22:39:05 -07:00
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if ( capabilities . includes ( 'net_admin' ) ) containerOptions . HostConfig . CapAdd . push ( 'NET_ADMIN' , 'NET_RAW' ) ;
if ( capabilities . includes ( 'mlock' ) ) containerOptions . HostConfig . CapAdd . push ( 'IPC_LOCK' ) ; // mlock prevents swapping
if ( ! capabilities . includes ( 'ping' ) ) containerOptions . HostConfig . CapDrop . push ( 'NET_RAW' ) ; // NET_RAW is included by default by Docker
2017-08-11 23:22:48 +01:00
2020-10-27 22:39:05 -07:00
if ( capabilities . includes ( 'vaapi' ) && safe . fs . existsSync ( '/dev/dri' ) ) {
containerOptions . HostConfig . Devices = [
{ PathOnHost : '/dev/dri' , PathInContainer : '/dev/dri' , CgroupPermissions : 'rwm' }
] ;
}
2020-08-14 18:48:53 -07:00
2020-10-27 22:39:05 -07:00
containerOptions = _ . extend ( containerOptions , options ) ;
2020-04-27 22:55:43 -07:00
2020-10-27 22:39:05 -07:00
gConnection . createContainer ( containerOptions , function ( error , container ) {
if ( error && error . statusCode === 409 ) return callback ( new BoxError ( BoxError . ALREADY _EXISTS , error ) ) ;
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2019-10-22 21:46:32 -07:00
2020-10-27 22:39:05 -07:00
callback ( null , container ) ;
} ) ;
2019-10-22 21:46:32 -07:00
} ) ;
2015-10-19 16:00:40 -07:00
} ) ;
2015-10-19 11:40:19 -07:00
}
2015-10-19 21:33:53 -07:00
function createContainer ( app , callback ) {
2020-04-27 22:55:43 -07:00
createSubcontainer ( app , app . id /* name */ , null /* cmd */ , { } /* options */ , callback ) ;
2015-10-19 21:33:53 -07:00
}
2015-10-19 15:39:26 -07:00
function startContainer ( containerId , callback ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof containerId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
container . start ( function ( error ) {
2019-09-23 12:13:21 -07:00
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND ) ) ;
2019-10-20 13:35:19 -07:00
if ( error && error . statusCode === 400 ) return callback ( new BoxError ( BoxError . BAD _FIELD , error ) ) ; // e.g start.sh is not executable
2019-12-20 10:29:29 -08:00
if ( error && error . statusCode !== 304 ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ; // 304 means already started
return callback ( null ) ;
} ) ;
}
function restartContainer ( containerId , callback ) {
assert . strictEqual ( typeof containerId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var container = gConnection . getContainer ( containerId ) ;
container . restart ( function ( error ) {
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND ) ) ;
if ( error && error . statusCode === 400 ) return callback ( new BoxError ( BoxError . BAD _FIELD , error ) ) ; // e.g start.sh is not executable
if ( error && error . statusCode !== 204 ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2015-10-19 11:40:19 -07:00
return callback ( null ) ;
} ) ;
}
2015-10-19 15:39:26 -07:00
function stopContainer ( containerId , callback ) {
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-19 15:39:26 -07:00
if ( ! containerId ) {
debug ( 'No previous container to stop' ) ;
2015-10-19 11:40:19 -07:00
return callback ( ) ;
}
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
var options = {
t : 10 // wait for 10 seconds before killing it
} ;
container . stop ( options , function ( error ) {
2019-10-22 21:46:32 -07:00
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Error stopping container:' + error . message ) ) ;
2015-10-19 11:40:19 -07:00
2020-05-24 12:30:48 -07:00
container . wait ( function ( error /*, data */ ) {
2019-10-22 21:46:32 -07:00
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Error waiting on container:' + error . message ) ) ;
2015-10-19 11:40:19 -07:00
return callback ( null ) ;
} ) ;
} ) ;
}
2020-08-18 22:35:08 -07:00
function deleteContainer ( containerId , callback ) { // id can also be name
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-19 15:39:26 -07:00
if ( containerId === null ) return callback ( null ) ;
2015-10-19 11:40:19 -07:00
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
var removeOptions = {
force : true , // kill container if it's running
v : true // removes volumes associated with the container (but not host mounts)
} ;
container . remove ( removeOptions , function ( error ) {
if ( error && error . statusCode === 404 ) return callback ( null ) ;
2019-10-23 09:53:46 -07:00
if ( error ) {
debug ( 'Error removing container %s : %j' , containerId , error ) ;
return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
}
2015-10-19 15:39:26 -07:00
2019-10-23 09:53:46 -07:00
callback ( null ) ;
2015-10-19 11:40:19 -07:00
} ) ;
}
2019-01-17 23:32:24 -08:00
function deleteContainers ( appId , options , callback ) {
2015-10-19 18:48:56 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2019-01-17 23:32:24 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-10-19 18:48:56 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-17 23:32:24 -08:00
let labels = [ 'appId=' + appId ] ;
if ( options . managedOnly ) labels . push ( 'isCloudronManaged=true' ) ;
2019-12-04 13:17:58 -08:00
gConnection . listContainers ( { all : 1 , filters : JSON . stringify ( { label : labels } ) } , function ( error , containers ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2015-10-19 18:48:56 -07:00
async . eachSeries ( containers , function ( container , iteratorDone ) {
deleteContainer ( container . Id , iteratorDone ) ;
} , callback ) ;
} ) ;
}
2015-10-20 00:05:07 -07:00
function stopContainers ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
gConnection . listContainers ( { all : 1 , filters : JSON . stringify ( { label : [ 'appId=' + appId ] } ) } , function ( error , containers ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2015-10-20 00:05:07 -07:00
async . eachSeries ( containers , function ( container , iteratorDone ) {
stopContainer ( container . Id , iteratorDone ) ;
} , callback ) ;
} ) ;
}
2015-10-19 15:39:26 -07:00
function deleteImage ( manifest , callback ) {
2015-10-19 15:51:02 -07:00
assert ( ! manifest || typeof manifest === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-19 11:40:19 -07:00
var dockerImage = manifest ? manifest . dockerImage : null ;
if ( ! dockerImage ) return callback ( null ) ;
2016-01-21 14:59:24 -08:00
var removeOptions = {
force : false , // might be shared with another instance of this app
noprune : false // delete untagged parents
} ;
2015-10-19 11:40:19 -07:00
2016-01-21 14:59:24 -08:00
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
2019-12-04 13:17:58 -08:00
gConnection . getImage ( dockerImage ) . remove ( removeOptions , function ( error ) {
2018-10-24 13:09:35 -07:00
if ( error && error . statusCode === 400 ) return callback ( null ) ; // invalid image format. this can happen if user installed with a bad --docker-image
if ( error && error . statusCode === 404 ) return callback ( null ) ; // not found
2016-01-21 14:59:24 -08:00
if ( error && error . statusCode === 409 ) return callback ( null ) ; // another container using the image
2015-10-19 11:40:19 -07:00
2019-10-23 09:24:51 -07:00
if ( error ) {
debug ( 'Error removing image %s : %j' , dockerImage , error ) ;
return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
}
2015-10-19 11:40:19 -07:00
2019-10-23 09:24:51 -07:00
callback ( null ) ;
2015-10-19 11:40:19 -07:00
} ) ;
}
2016-02-18 15:39:27 +01:00
function getContainerIdByIp ( ip , callback ) {
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
gConnection . getNetwork ( 'cloudron' ) . inspect ( function ( error , bridge ) {
2019-12-04 10:29:06 -08:00
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Unable to find the cloudron network' ) ) ;
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2016-02-18 15:39:27 +01:00
var containerId ;
for ( var id in bridge . Containers ) {
2017-11-11 16:54:43 -08:00
if ( bridge . Containers [ id ] . IPv4Address . indexOf ( ip + '/16' ) === 0 ) {
2016-02-18 15:39:27 +01:00
containerId = id ;
break ;
}
}
2019-12-04 10:29:06 -08:00
if ( ! containerId ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'No container with that ip' ) ) ;
2016-02-18 15:39:27 +01:00
callback ( null , containerId ) ;
} ) ;
}
2016-04-18 10:32:22 -07:00
2017-08-11 22:04:40 +02:00
function inspect ( containerId , callback ) {
assert . strictEqual ( typeof containerId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2017-08-11 22:04:40 +02:00
container . inspect ( function ( error , result ) {
2020-07-30 11:29:43 -07:00
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND , ` Unable to find container ${ containerId } ` ) ) ;
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2018-11-19 10:19:46 +01:00
2018-11-28 10:39:12 +01:00
callback ( null , result ) ;
} ) ;
}
2020-11-18 23:24:34 -08:00
function getContainerIp ( containerId , callback ) {
assert . strictEqual ( typeof containerId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( constants . TEST ) return callback ( null , '127.0.5.5' ) ;
inspect ( containerId , function ( error , result ) {
if ( error ) return callback ( error ) ;
const ip = safe . query ( result , 'NetworkSettings.Networks.cloudron.IPAddress' , null ) ;
if ( ! ip ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Error getting container IP' ) ) ;
callback ( null , ip ) ;
} ) ;
}
2019-12-04 13:17:58 -08:00
function execContainer ( containerId , options , callback ) {
assert . strictEqual ( typeof containerId , 'string' ) ;
2019-03-06 11:54:37 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2019-03-06 11:54:37 -08:00
2019-12-04 13:17:58 -08:00
container . exec ( options . execOptions , function ( error , exec ) {
if ( error && error . statusCode === 409 ) return callback ( new BoxError ( BoxError . BAD _STATE , error . message ) ) ; // container restarting/not running
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
exec . start ( options . startOptions , function ( error , stream /* in hijacked mode, this is a net.socket */ ) {
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
if ( options . rows && options . columns ) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout ( function ( ) {
exec . resize ( { h : options . rows , w : options . columns } , function ( error ) { if ( error ) debug ( 'Error resizing console' , error ) ; } ) ;
} , 2000 ) ;
}
callback ( null , stream ) ;
} ) ;
} ) ;
}
function getEvents ( options , callback ) {
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
gConnection . getEvents ( options , function ( error , stream ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2019-03-06 11:54:37 -08:00
callback ( null , stream ) ;
} ) ;
}
2018-11-28 10:39:12 +01:00
function memoryUsage ( containerId , callback ) {
assert . strictEqual ( typeof containerId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
var container = gConnection . getContainer ( containerId ) ;
2018-11-28 10:39:12 +01:00
container . stats ( { stream : false } , function ( error , result ) {
2019-09-23 12:13:21 -07:00
if ( error && error . statusCode === 404 ) return callback ( new BoxError ( BoxError . NOT _FOUND ) ) ;
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2018-11-28 10:39:12 +01:00
2017-08-11 22:04:40 +02:00
callback ( null , result ) ;
} ) ;
}
2020-04-29 21:28:46 -07:00
function createVolume ( name , volumeDataDir , labels , callback ) {
2018-09-13 13:55:49 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2019-01-13 19:18:06 -08:00
assert . strictEqual ( typeof volumeDataDir , 'string' ) ;
2020-04-29 21:28:46 -07:00
assert . strictEqual ( typeof labels , 'object' ) ;
2018-09-13 13:55:49 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
const volumeOptions = {
Name : name ,
Driver : 'local' ,
DriverOpts : { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
type : 'none' ,
device : volumeDataDir ,
o : 'bind'
} ,
2020-04-29 21:28:46 -07:00
Labels : labels
2018-09-13 13:55:49 -07:00
} ;
2018-12-20 14:33:29 -08:00
// requires sudo because the path can be outside appsdata
shell . sudo ( 'createVolume' , [ MKDIRVOLUME _CMD , volumeDataDir ] , { } , function ( error ) {
2019-12-04 10:29:06 -08:00
if ( error ) return callback ( new BoxError ( BoxError . FS _ERROR , ` Error creating app data dir: ${ error . message } ` ) ) ;
2018-09-13 13:55:49 -07:00
2019-12-04 13:17:58 -08:00
gConnection . createVolume ( volumeOptions , function ( error ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2018-09-13 13:55:49 -07:00
2020-04-27 22:55:43 -07:00
callback ( ) ;
2018-09-13 13:55:49 -07:00
} ) ;
} ) ;
}
2020-04-29 21:28:46 -07:00
function clearVolume ( name , options , callback ) {
2018-09-15 17:05:04 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2019-01-18 14:48:31 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-09-15 17:05:04 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
let volume = gConnection . getVolume ( name ) ;
2019-01-13 19:18:06 -08:00
volume . inspect ( function ( error , v ) {
if ( error && error . statusCode === 404 ) return callback ( ) ;
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , error ) ) ;
2019-01-13 19:18:06 -08:00
const volumeDataDir = v . Options . device ;
2019-10-22 21:46:32 -07:00
shell . sudo ( 'clearVolume' , [ CLEARVOLUME _CMD , options . removeDirectory ? 'rmdir' : 'clear' , volumeDataDir ] , { } , function ( error ) {
if ( error ) return callback ( new BoxError ( BoxError . FS _ERROR , error ) ) ;
callback ( ) ;
} ) ;
2019-01-13 19:18:06 -08:00
} ) ;
2018-09-15 17:05:04 -07:00
}
2019-01-17 09:53:51 -08:00
// this only removes the volume and not the data
2020-04-29 21:28:46 -07:00
function removeVolume ( name , callback ) {
2018-09-13 13:55:49 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
let volume = gConnection . getVolume ( name ) ;
2019-01-15 09:36:32 -08:00
volume . remove ( function ( error ) {
2020-04-29 21:28:46 -07:00
if ( error && error . statusCode !== 404 ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , ` removeVolume: Error removing volume: ${ error . message } ` ) ) ;
2019-01-13 19:18:06 -08:00
2019-01-15 09:36:32 -08:00
callback ( ) ;
2018-09-13 13:55:49 -07:00
} ) ;
}
2019-08-12 20:38:24 -07:00
function info ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2019-12-04 13:17:58 -08:00
gConnection . info ( function ( error , result ) {
2019-09-23 23:27:32 -07:00
if ( error ) return callback ( new BoxError ( BoxError . DOCKER _ERROR , 'Error connecting to docker' ) ) ;
2019-08-12 20:38:24 -07:00
callback ( null , result ) ;
} ) ;
}
2021-01-20 12:01:15 -08:00
function update ( name , memory , memorySwap , callback ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof memory , 'number' ) ;
assert . strictEqual ( typeof memorySwap , 'number' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const args = ` update --memory ${ memory } --memory-swap ${ memorySwap } ${ name } ` . split ( ' ' ) ;
// scale back db containers, if possible. this is retried because updating memory constraints can fail
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
async . retry ( { times : 10 , interval : 60 * 1000 } , function ( retryCallback ) {
shell . spawn ( ` update( ${ name } ) ` , '/usr/bin/docker' , args , { } , retryCallback ) ;
} , callback ) ;
}