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 ,
injectPrivateFields ,
removePrivateFields ,
ping ,
info ,
2022-10-11 22:38:26 +02:00
df ,
2020-11-18 23:24:34 -08:00
downloadImage ,
createContainer ,
startContainer ,
restartContainer ,
stopContainer ,
stopContainers ,
deleteContainer ,
deleteImage ,
deleteContainers ,
createSubcontainer ,
inspect ,
getContainerIp ,
getEvents ,
memoryUsage ,
2022-05-16 10:26:30 -07:00
2021-08-19 12:32:23 -07:00
update ,
2022-05-16 10:26:30 -07:00
createExec ,
startExec ,
getExec ,
resizeExec
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
assert = require ( 'assert' ) ,
2019-09-23 12:13:21 -07:00
BoxError = require ( './boxerror.js' ) ,
2016-04-18 16:30:58 -07:00
constants = require ( './constants.js' ) ,
2019-12-06 13:52:43 -08:00
debug = require ( 'debug' ) ( 'box:docker' ) ,
2022-04-15 07:52:35 -05:00
delay = require ( './delay.js' ) ,
2019-12-04 13:17:58 -08:00
Docker = require ( 'dockerode' ) ,
2022-11-28 22:32:34 +01:00
paths = require ( './paths.js' ) ,
2023-03-09 21:06:54 +01:00
promiseRetry = require ( './promise-retry.js' ) ,
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' ) ,
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-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
2021-08-25 19:41:46 -07:00
async function testRegistryConfig ( config ) {
2021-03-02 18:21:35 -08:00
assert . strictEqual ( typeof config , 'object' ) ;
2021-08-25 19:41:46 -07:00
if ( config . provider === 'noop' ) return ;
2019-10-23 06:11:17 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( gConnection . checkAuth ( config ) ) ; // this returns a 500 even for auth errors
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid serverAddress: ${ error . message } ` ) ;
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 ;
}
2021-08-26 21:14:49 -07:00
async function ping ( ) {
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
2021-08-26 21:14:49 -07:00
const [ error , result ] = await safe ( connection . ping ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
if ( Buffer . isBuffer ( result ) && result . toString ( 'utf8' ) === 'OK' ) return ; // sometimes it returns buffer
if ( result === 'OK' ) return ;
2018-11-19 10:19:46 +01:00
2021-08-26 21:14:49 -07:00
throw new BoxError ( BoxError . DOCKER _ERROR , 'Unable to ping the docker daemon' ) ;
2018-11-19 10:19:46 +01:00
}
2021-08-25 19:41:46 -07:00
async function getRegistryConfig ( image ) {
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 ( '/' ) ;
2021-08-25 19:41:46 -07:00
if ( parts . length === 1 || ( parts [ 0 ] . match ( /[.:]/ ) === null ) ) return null ; // public docker registry
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
const registryConfig = await settings . getRegistryConfig ( ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
// 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
} ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
return auth ;
}
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
async function pullImage ( manifest ) {
const config = await getRegistryConfig ( manifest . dockerImage ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
debug ( ` pullImage: will pull ${ manifest . dockerImage } . auth: ${ config ? 'yes' : 'no' } ` ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
const [ error , stream ] = await safe ( gConnection . pull ( manifest . dockerImage , { authconfig : config } ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to pull image ${ manifest . dockerImage } . message: ${ error . message } statusCode: ${ error . statusCode } ` ) ;
if ( error ) throw 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
2021-08-25 19:41:46 -07:00
return new Promise ( ( resolve , reject ) => {
2023-03-09 21:06:54 +01:00
// https://github.com/dotcloud/docker/issues/1074 says each status message is emitted as a chunk
let layerError = null ;
2021-08-25 19:41:46 -07:00
stream . on ( 'data' , function ( chunk ) {
2022-04-14 17:41:41 -05:00
const data = safe . JSON . parse ( chunk ) || { } ;
2021-08-25 19:41:46 -07:00
debug ( 'pullImage: %j' , data ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
// The data.status here is useless because this is per layer as opposed to per image
2023-03-09 21:06:54 +01:00
if ( ! data . status && data . error ) { // data is { errorDetail: { message: xx } , error: xx }
debug ( ` pullImage error ${ manifest . dockerImage } : ${ data . errorDetail . message } ` ) ;
layerError = data . errorDetail ;
2021-08-25 19:41:46 -07:00
}
} ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
stream . on ( 'end' , function ( ) {
2023-03-09 21:06:54 +01:00
debug ( ` downloaded image ${ manifest . dockerImage } . error: ${ ! ! layerError } ` ) ;
if ( ! layerError ) return resolve ( ) ;
reject ( new BoxError ( layerError . includes ( 'no space' ) ? BoxError . FS _ERROR : BoxError . DOCKER _ERROR , layerError . message ) ) ;
2021-08-25 19:41:46 -07:00
} ) ;
2015-10-19 11:40:19 -07:00
2023-03-09 21:06:54 +01:00
stream . on ( 'error' , function ( error ) { // this is only hit for stream error and not for some download error
2021-08-25 19:41:46 -07:00
debug ( 'error pulling image %s: %j' , manifest . dockerImage , error ) ;
reject ( new BoxError ( BoxError . DOCKER _ERROR , error . message ) ) ;
2015-10-19 11:40:19 -07:00
} ) ;
} ) ;
}
2021-08-25 19:41:46 -07:00
async function downloadImage ( manifest ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2021-08-25 19:41:46 -07:00
debug ( ` downloadImage ${ 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
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( image . inspect ( ) ) ;
if ( ! error && result ) return ; // image is already present locally
2015-10-19 11:40:19 -07:00
2023-03-10 12:10:36 +01:00
await promiseRetry ( { times : 10 , interval : 5000 , debug , retry : ( pullError ) => pullError . reason !== BoxError . NOT _FOUND && pullError . reason !== BoxError . FS _ERROR } , async ( ) => {
2023-03-09 21:06:54 +01:00
await pullImage ( manifest ) ;
} ) ;
2015-10-19 11:40:19 -07:00
}
2021-05-11 17:50:48 -07:00
async function getVolumeMounts ( app ) {
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-10-27 22:39:05 -07:00
2021-02-17 22:53:50 -08:00
let mounts = [ ] ;
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
if ( app . mounts . length === 0 ) return [ ] ;
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
const result = await volumes . list ( ) ;
2021-02-17 22:53:50 -08:00
2021-05-11 17:50:48 -07:00
let volumesById = { } ;
result . forEach ( r => volumesById [ r . id ] = r ) ;
2020-10-27 22:39:05 -07:00
2021-05-11 17:50:48 -07:00
for ( const mount of app . mounts ) {
const volume = volumesById [ mount . volumeId ] ;
2021-02-17 22:53:50 -08:00
2021-05-11 17:50:48 -07:00
mounts . push ( {
Source : volume . hostPath ,
Target : ` /media/ ${ volume . name } ` ,
Type : 'bind' ,
ReadOnly : mount . readOnly
} ) ;
}
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
return mounts ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getAddonMounts ( app ) {
2021-02-17 22:53:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
let mounts = [ ] ;
const addons = app . manifest . addons ;
2021-08-25 19:41:46 -07:00
if ( ! addons ) return mounts ;
2021-02-17 22:53:50 -08:00
2021-08-25 19:41:46 -07:00
for ( const addon of Object . keys ( addons ) ) {
2021-02-17 22:53:50 -08:00
switch ( addon ) {
2022-06-06 15:59:50 -07:00
case 'localstorage' : {
const storageDir = await apps . getStorageDir ( app ) ;
2021-02-17 22:53:50 -08:00
mounts . push ( {
Target : '/app/data' ,
2022-06-06 15:59:50 -07:00
Source : storageDir ,
Type : 'bind' ,
2021-02-17 22:53:50 -08:00
ReadOnly : false
} ) ;
2021-08-25 19:41:46 -07:00
break ;
2022-06-06 15:59:50 -07:00
}
2021-08-17 14:04:29 -07:00
case 'tls' : {
2022-11-28 22:32:34 +01:00
const certificateDir = ` ${ paths . PLATFORM _DATA _DIR } /tls/ ${ app . id } ` ;
2021-08-17 14:04:29 -07:00
mounts . push ( {
2022-11-28 22:32:34 +01:00
Target : '/etc/certs' ,
Source : certificateDir ,
2021-08-17 14:04:29 -07:00
Type : 'bind' ,
ReadOnly : true
2021-02-17 22:53:50 -08:00
} ) ;
2021-08-25 19:41:46 -07:00
break ;
2021-08-17 14:04:29 -07:00
}
2021-02-17 22:53:50 -08:00
default :
2021-08-25 19:41:46 -07:00
break ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
}
return mounts ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getMounts ( app ) {
2021-02-17 22:53:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-08-25 19:41:46 -07:00
const volumeMounts = await getVolumeMounts ( app ) ;
const addonMounts = await getAddonMounts ( app ) ;
return volumeMounts . concat ( addonMounts ) ;
2020-04-29 21:55:21 -07:00
}
2021-08-10 21:59:58 -07:00
function getAddresses ( ) {
const deviceLinks = safe . fs . readdirSync ( '/sys/class/net' ) ; // https://man7.org/linux/man-pages/man5/sysfs.5.html
if ( ! deviceLinks ) return [ ] ;
const devices = deviceLinks . map ( d => { return { name : d , link : safe . fs . readlinkSync ( ` /sys/class/net/ ${ d } ` ) } ; } ) ;
const physicalDevices = devices . filter ( d => d . link && ! d . link . includes ( 'virtual' ) ) ;
const addresses = [ ] ;
for ( const phy of physicalDevices ) {
2022-03-01 12:12:46 -08:00
const inet = safe . JSON . parse ( safe . child _process . execSync ( ` ip -f inet -j addr show dev ${ phy . name } scope global ` , { encoding : 'utf8' } ) ) ;
2022-02-17 11:56:08 -08:00
for ( const r of inet ) {
const address = safe . query ( r , 'addr_info[0].local' ) ;
if ( address ) addresses . push ( address ) ;
}
2022-03-01 12:12:46 -08:00
const inet6 = safe . JSON . parse ( safe . child _process . execSync ( ` ip -f inet6 -j addr show dev ${ phy . name } scope global ` , { encoding : 'utf8' } ) ) ;
2022-02-17 11:56:08 -08:00
for ( const r of inet6 ) {
docker: loop through the ip net addr output
all of a sudden, my linux box has the actual address in [1].
ip -f inet -j addr show wlp2s0
[{
"addr_info": [{}]
},{
"ifindex": 3,
"ifname": "wlp2s0",
"flags": ["BROADCAST","MULTICAST","UP","LOWER_UP"],
"mtu": 1500,
"qdisc": "mq",
"operstate": "UP",
"group": "default",
"txqlen": 1000,
"addr_info": [{
"family": "inet",
"local": "192.168.1.8",
"prefixlen": 24,
"broadcast": "192.168.1.255",
"scope": "global",
"dynamic": true,
"noprefixroute": true,
"label": "wlp2s0",
"valid_life_time": 78146,
"preferred_life_time": 78146
}]
},{
"addr_info": [{}]
},{
"addr_info": [{}]
}
]
2021-12-22 16:48:00 -08:00
const address = safe . query ( r , 'addr_info[0].local' ) ;
if ( address ) addresses . push ( address ) ;
}
2020-11-18 11:43:28 -08:00
}
2021-08-10 21:59:58 -07:00
return addresses ;
2020-11-18 11:43:28 -08:00
}
2021-08-25 19:41:46 -07:00
async function createSubcontainer ( app , name , cmd , options ) {
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' ) ;
2021-05-02 11:26:08 -07:00
assert ( ! cmd || Array . isArray ( cmd ) ) ;
2020-04-27 22:55:43 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-10-19 11:40:19 -07:00
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
2021-08-25 19:41:46 -07:00
const manifest = app . manifest ;
const exposedPorts = { } , dockerPortBindings = { } ;
const domain = app . fqdn ;
2019-06-03 13:45:03 -07:00
2021-08-25 19:41:46 -07:00
const 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 } ` ,
2021-12-06 17:43:50 -08:00
` CLOUDRON_WEBADMIN_ORIGIN= ${ settings . dashboardOrigin ( ) } ` ,
` CLOUDRON_API_ORIGIN= ${ settings . dashboardOrigin ( ) } ` ,
` CLOUDRON_APP_ORIGIN=https:// ${ domain } ` ,
` CLOUDRON_APP_DOMAIN= ${ domain } `
2015-10-19 11:40:19 -07:00
] ;
2022-01-21 11:24:51 -08:00
const secondaryDomainsEnv = app . secondaryDomains . map ( sd => ` ${ sd . environmentVariable } = ${ sd . fqdn } ` ) ;
2021-08-25 19:41:46 -07:00
const portEnv = [ ] ;
for ( const 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
2021-08-25 19:41:46 -07:00
const containerPort = ports [ portName ] . containerPort || hostPort ;
2018-08-12 22:47:59 -07:00
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
2022-02-09 17:47:48 -08:00
const hostIps = hostPort === 53 ? getAddresses ( ) : [ '0.0.0.0' , '::0' ] ; // port 53 is special because it is possibly taken by systemd-resolved
2021-08-10 21:59:58 -07:00
dockerPortBindings [ ` ${ containerPort } / ${ portType } ` ] = hostIps . map ( hip => { return { HostIp : hip , HostPort : hostPort + '' } ; } ) ;
2015-10-19 11:40:19 -07:00
}
2018-10-11 16:18:38 -07:00
2021-08-25 19:41:46 -07:00
const appEnv = [ ] ;
2018-10-11 14:07:43 -07:00
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-08-25 19:41:46 -07:00
const mounts = await getMounts ( app ) ;
const addonEnv = await services . getEnvironment ( app ) ;
2022-10-24 22:34:06 +02:00
const runtimeVolumes = {
'/tmp' : { } ,
2022-10-24 23:58:20 +02:00
'/run' : { } ,
'/home/cloudron/.cache' : { } ,
'/root/.cache' : { }
2022-10-24 22:34:06 +02:00
} ;
if ( app . manifest . runtimeDirs ) {
app . manifest . runtimeDirs . forEach ( dir => runtimeVolumes [ dir ] = { } ) ;
}
2021-08-25 19:41:46 -07:00
let containerOptions = {
name : name , // for referencing containers
Tty : isAppContainer ,
Image : app . manifest . dockerImage ,
Cmd : ( isAppContainer && app . debugMode && app . debugMode . cmd ) ? app . debugMode . cmd : cmd ,
2022-01-21 11:24:51 -08:00
Env : stdEnv . concat ( addonEnv ) . concat ( portEnv ) . concat ( appEnv ) . concat ( secondaryDomainsEnv ) ,
2021-08-25 19:41:46 -07:00
ExposedPorts : isAppContainer ? exposedPorts : { } ,
2022-10-24 22:34:06 +02:00
Volumes : runtimeVolumes ,
2021-08-25 19:41:46 -07:00
Labels : {
'fqdn' : app . fqdn ,
'appId' : app . id ,
'isSubcontainer' : String ( ! isAppContainer ) ,
'isCloudronManaged' : String ( true )
} ,
HostConfig : {
Mounts : mounts ,
LogConfig : {
Type : 'syslog' ,
Config : {
'tag' : app . id ,
'syslog-address' : 'udp://127.0.0.1:2514' , // see apps.js:validatePortBindings()
'syslog-format' : 'rfc5424'
}
2021-08-19 21:39:27 -07:00
} ,
2022-11-04 15:09:37 +01:00
Memory : await system . getMemoryAllocation ( memoryLimit ) ,
2021-08-25 19:41:46 -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
2021-08-19 21:39:27 -07:00
} ,
2021-08-25 19:41:46 -07:00
CpuShares : app . cpuShares ,
VolumesFrom : isAppContainer ? null : [ app . containerId + ':rw' ] ,
SecurityOpt : [ 'apparmor=docker-cloudron-app' ] ,
2022-04-28 17:04:23 -07:00
CapAdd : [ ] ,
2022-03-10 11:59:41 -08:00
CapDrop : [ ] ,
Sysctls : { }
2021-08-25 19:41:46 -07:00
}
} ;
2017-08-11 23:22:48 +01:00
2021-08-25 19:41:46 -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
containerOptions . HostConfig . Dns = [ '172.18.0.1' ] ; // use internal dns
containerOptions . HostConfig . DnsSearch = [ '.' ] ; // use internal dns
containerOptions . NetworkingConfig = {
EndpointsConfig : {
cloudron : {
IPAMConfig : {
IPv4Address : app . containerIp
} ,
Aliases : [ name ] // adds hostname entry with container name
2021-08-19 21:39:27 -07:00
}
2021-08-25 19:41:46 -07:00
}
} ;
} else {
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
2021-08-25 19:41:46 -07:00
const capabilities = manifest . capabilities || [ ] ;
2020-06-30 07:31:24 -07:00
2021-08-25 19:41:46 -07:00
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
2022-04-28 17:04:23 -07:00
if ( capabilities . includes ( 'net_admin' ) ) {
containerOptions . HostConfig . CapAdd . push ( 'NET_ADMIN' , 'NET_RAW' ) ;
// ipv6 for new interfaces is disabled in the container. this prevents the openvpn tun device having ipv6
// See https://github.com/moby/moby/issues/20569 and https://github.com/moby/moby/issues/33099
containerOptions . HostConfig . Sysctls [ 'net.ipv6.conf.all.disable_ipv6' ] = '0' ;
2022-05-19 17:10:05 -07:00
containerOptions . HostConfig . Sysctls [ 'net.ipv6.conf.all.forwarding' ] = '1' ;
2022-04-28 17:04:23 -07:00
}
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
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
2021-08-25 19:41:46 -07:00
containerOptions = _ . extend ( containerOptions , options ) ;
2020-04-27 22:55:43 -07:00
2021-08-25 19:41:46 -07:00
const [ createError , container ] = await safe ( gConnection . createContainer ( containerOptions ) ) ;
if ( createError && createError . statusCode === 409 ) throw new BoxError ( BoxError . ALREADY _EXISTS , createError ) ;
if ( createError ) throw new BoxError ( BoxError . DOCKER _ERROR , createError ) ;
2019-10-22 21:46:32 -07:00
2021-08-25 19:41:46 -07:00
return container ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function createContainer ( app ) {
return await createSubcontainer ( app , app . id /* name */ , null /* cmd */ , { } /* options */ ) ;
2015-10-19 21:33:53 -07:00
}
2021-08-25 19:41:46 -07:00
async function startContainer ( containerId ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . start ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
if ( error && error . statusCode === 400 ) throw new BoxError ( BoxError . BAD _FIELD , error ) ; // e.g start.sh is not executable
if ( error && error . statusCode !== 304 ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ; // 304 means already started
2019-12-20 10:29:29 -08:00
}
2021-08-25 19:41:46 -07:00
async function restartContainer ( containerId ) {
2019-12-20 10:29:29 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . restart ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
if ( error && error . statusCode === 400 ) throw new BoxError ( BoxError . BAD _FIELD , error ) ; // e.g start.sh is not executable
if ( error && error . statusCode !== 204 ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainer ( containerId ) {
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
2015-10-19 15:39:26 -07:00
if ( ! containerId ) {
debug ( 'No previous container to stop' ) ;
2021-08-25 19:41:46 -07:00
return ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const options = {
2015-10-19 11:40:19 -07:00
t : 10 // wait for 10 seconds before killing it
} ;
2021-08-25 19:41:46 -07:00
let [ error ] = await safe ( container . stop ( options ) ) ;
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error stopping container:' + error . message ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
[ error ] = await safe ( container . wait ( ) ) ;
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error waiting on container:' + error . message ) ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainer ( containerId ) { // id can also be name
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
2021-08-25 19:41:46 -07:00
if ( containerId === null ) return null ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const removeOptions = {
2015-10-19 11:40:19 -07:00
force : true , // kill container if it's running
v : true // removes volumes associated with the container (but not host mounts)
} ;
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . remove ( removeOptions ) ) ;
if ( error && error . statusCode === 404 ) return ;
2015-10-19 15:39:26 -07:00
2021-08-25 19:41:46 -07:00
if ( error ) {
debug ( 'Error removing container %s : %j' , containerId , error ) ;
throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainers ( appId , options ) {
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
2021-08-25 19:41:46 -07:00
const labels = [ 'appId=' + appId ] ;
2019-01-17 23:32:24 -08:00
if ( options . managedOnly ) labels . push ( 'isCloudronManaged=true' ) ;
2021-08-25 19:41:46 -07:00
const [ error , containers ] = await safe ( gConnection . listContainers ( { all : 1 , filters : JSON . stringify ( { label : labels } ) } ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-19 18:48:56 -07:00
2021-08-25 19:41:46 -07:00
for ( const container of containers ) {
await deleteContainer ( container . Id ) ;
}
2015-10-19 18:48:56 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainers ( appId ) {
2015-10-20 00:05:07 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const [ error , containers ] = await safe ( gConnection . listContainers ( { all : 1 , filters : JSON . stringify ( { label : [ 'appId=' + appId ] } ) } ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-20 00:05:07 -07:00
2021-08-25 19:41:46 -07:00
for ( const container of containers ) {
await stopContainer ( container . Id ) ;
}
2015-10-20 00:05:07 -07:00
}
2021-08-26 18:34:32 -07:00
async function deleteImage ( manifest ) {
2015-10-19 15:51:02 -07:00
assert ( ! manifest || typeof manifest === 'object' ) ;
2021-06-16 11:46:18 -07:00
const dockerImage = manifest ? manifest . dockerImage : null ;
2021-08-26 18:34:32 -07:00
if ( ! dockerImage ) return ;
2022-03-24 10:03:40 -07:00
if ( dockerImage . includes ( '//' ) || dockerImage . startsWith ( '/' ) ) return ; // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module (https://github.com/apocas/dockerode/issues/548)
2015-10-19 11:40:19 -07:00
2021-06-16 11:46:18 -07:00
const removeOptions = {
2016-01-21 14:59:24 -08:00
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
2021-08-26 18:34:32 -07:00
const [ error ] = await safe ( gConnection . getImage ( dockerImage ) . remove ( removeOptions ) ) ;
if ( error && error . statusCode === 400 ) return ; // invalid image format. this can happen if user installed with a bad --docker-image
if ( error && error . statusCode === 404 ) return ; // not found
if ( error && error . statusCode === 409 ) return ; // another container using the image
2015-10-19 11:40:19 -07:00
2021-08-26 18:34:32 -07:00
if ( error ) {
debug ( 'Error removing image %s : %j' , dockerImage , error ) ;
throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2015-10-19 11:40:19 -07:00
}
2016-02-18 15:39:27 +01:00
2021-08-25 19:41:46 -07:00
async function inspect ( containerId ) {
2017-08-11 22:04:40 +02:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2017-08-11 22:04:40 +02:00
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( container . inspect ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to find container ${ containerId } ` ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2018-11-19 10:19:46 +01:00
2021-08-25 19:41:46 -07:00
return result ;
2018-11-28 10:39:12 +01:00
}
2021-08-25 19:41:46 -07:00
async function getContainerIp ( containerId ) {
2020-11-18 23:24:34 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
if ( constants . TEST ) return '127.0.5.5' ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
const result = await inspect ( containerId ) ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
const ip = safe . query ( result , 'NetworkSettings.Networks.cloudron.IPAddress' , null ) ;
if ( ! ip ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error getting container IP' ) ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
return ip ;
2020-11-18 23:24:34 -08:00
}
2022-05-16 10:26:30 -07:00
async function createExec ( containerId , options ) {
2019-12-04 13:17:58 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2019-03-06 11:54:37 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2022-05-16 10:26:30 -07:00
const [ error , exec ] = await safe ( container . exec ( options ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
2021-08-25 19:41:46 -07:00
if ( error && error . statusCode === 409 ) throw new BoxError ( BoxError . BAD _STATE , error . message ) ; // container restarting/not running
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
return exec . id ;
}
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
async function startExec ( execId , options ) {
assert . strictEqual ( typeof execId , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
const exec = gConnection . getExec ( execId ) ;
const [ error , stream ] = await safe ( exec . start ( options ) ) ; /* in hijacked mode, stream is a net.socket */
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2021-08-25 19:41:46 -07:00
return stream ;
2019-12-04 13:17:58 -08:00
}
2022-05-16 10:26:30 -07:00
async function getExec ( execId ) {
assert . strictEqual ( typeof execId , 'string' ) ;
const exec = gConnection . getExec ( execId ) ;
const [ error , result ] = await safe ( exec . inspect ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to find exec container ${ execId } ` ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
return { exitCode : result . ExitCode , running : result . Running } ;
}
async function resizeExec ( execId , options ) {
assert . strictEqual ( typeof execId , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const exec = gConnection . getExec ( execId ) ;
const [ error ] = await safe ( exec . resize ( options ) ) ; // { h, w }
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2021-08-25 19:41:46 -07:00
async function getEvents ( options ) {
2019-12-04 13:17:58 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-08-25 19:41:46 -07:00
const [ error , stream ] = await safe ( gConnection . getEvents ( options ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
return stream ;
2019-03-06 11:54:37 -08:00
}
2021-08-25 19:41:46 -07:00
async function memoryUsage ( containerId ) {
2018-11-28 10:39:12 +01:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( container . stats ( { stream : false } ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
return result ;
2017-08-11 22:04:40 +02:00
}
2021-08-25 19:41:46 -07:00
async function info ( ) {
const [ error , result ] = await safe ( gConnection . info ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error connecting to docker' ) ;
return result ;
2019-08-12 20:38:24 -07:00
}
2021-01-20 12:01:15 -08:00
2022-10-11 22:38:26 +02:00
async function df ( ) {
const [ error , result ] = await safe ( gConnection . df ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error connecting to docker' ) ;
return result ;
}
2021-08-25 19:41:46 -07:00
async function update ( name , memory , memorySwap ) {
2021-01-20 12:01:15 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof memory , 'number' ) ;
assert . strictEqual ( typeof memorySwap , 'number' ) ;
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
2021-08-25 19:41:46 -07:00
for ( let times = 0 ; times < 10 ; times ++ ) {
const [ error ] = await safe ( shell . promises . spawn ( ` update( ${ name } ) ` , '/usr/bin/docker' , args , { } ) ) ;
if ( ! error ) return ;
await delay ( 60 * 1000 ) ;
}
throw new BoxError ( BoxError . DOCKER _ERROR , 'Unable to update container' ) ;
2021-01-20 12:01:15 -08:00
}