Skip to content

Commit f2c5f53

Browse files
authored
secure docker (#1291)
* secure docker * allow additional CapDrops for logging/debug * improve image build
1 parent 5e9d806 commit f2c5f53

1 file changed

Lines changed: 84 additions & 26 deletions

File tree

src/components/c2d/compute_engine_docker.ts

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,7 @@ export class C2DEngineDocker extends C2DEngine {
16361636
// create the container
16371637
const mountVols: any = { '/data': {} }
16381638
const hostConfig: HostConfig = {
1639+
NetworkMode: 'none', // no network inside the container
16391640
Mounts: [
16401641
{
16411642
Type: 'volume',
@@ -1694,9 +1695,11 @@ export class C2DEngineDocker extends C2DEngine {
16941695
if (advancedConfig.SecurityOpt)
16951696
containerInfo.HostConfig.SecurityOpt = advancedConfig.SecurityOpt
16961697
if (advancedConfig.Binds) containerInfo.HostConfig.Binds = advancedConfig.Binds
1698+
containerInfo.HostConfig.CapDrop = ['ALL']
1699+
for (const cap of advancedConfig.CapDrop ?? []) {
1700+
containerInfo.HostConfig.CapDrop.push(cap)
1701+
}
16971702
if (advancedConfig.CapAdd) containerInfo.HostConfig.CapAdd = advancedConfig.CapAdd
1698-
if (advancedConfig.CapDrop)
1699-
containerInfo.HostConfig.CapDrop = advancedConfig.CapDrop
17001703
if (advancedConfig.IpcMode)
17011704
containerInfo.HostConfig.IpcMode = advancedConfig.IpcMode
17021705
if (advancedConfig.ShmSize)
@@ -2337,6 +2340,9 @@ export class C2DEngineDocker extends C2DEngine {
23372340
const job = JSON.parse(JSON.stringify(originaljob)) as DBComputeJob
23382341
const imageLogFile =
23392342
this.getC2DConfig().tempFolder + '/' + job.jobId + '/data/logs/image.log'
2343+
const controller = new AbortController()
2344+
const timeoutMs = 5 * 60 * 1000
2345+
const timer = setTimeout(() => controller.abort(), timeoutMs)
23402346
try {
23412347
const pack = tarStream.pack()
23422348

@@ -2350,52 +2356,104 @@ export class C2DEngineDocker extends C2DEngine {
23502356
}
23512357
pack.finalize()
23522358

2353-
// Build the image using the tar stream as context
2354-
const buildStream = await this.docker.buildImage(pack, {
2355-
t: job.containerImage
2356-
})
2357-
2358-
// Optional: listen to build output
2359-
buildStream.on('data', (data) => {
2359+
// Build the image using the tar stream as context (Node IncomingMessage extends stream.Readable)
2360+
const buildStream = (await this.docker.buildImage(pack, {
2361+
t: job.containerImage,
2362+
memory: 1024 * 1024 * 1024, // 1GB RAM in bytes
2363+
memswap: -1, // Disable swap
2364+
cpushares: 512, // CPU Shares (default is 1024)
2365+
cpuquota: 50000, // 50% of one CPU (100000 = 1 CPU)
2366+
cpuperiod: 100000, // Default period
2367+
nocache: true, // prevent cache poison
2368+
abortSignal: controller.signal
2369+
})) as Readable
2370+
2371+
const onBuildData = (data: Buffer) => {
23602372
try {
23612373
const text = JSON.parse(data.toString('utf8'))
2362-
CORE_LOGGER.debug(
2363-
"Building image for jobId '" + job.jobId + "': " + text.stream.trim()
2364-
)
2365-
appendFileSync(imageLogFile, String(text.stream))
2374+
if (text && text.stream && typeof text.stream === 'string') {
2375+
CORE_LOGGER.debug(
2376+
"Building image for jobId '" + job.jobId + "': " + text.stream.trim()
2377+
)
2378+
appendFileSync(imageLogFile, String(text.stream))
2379+
}
23662380
} catch (e) {
23672381
// console.log('non json build data: ', data.toString('utf8'))
23682382
}
2369-
})
2383+
}
2384+
buildStream.on('data', onBuildData)
23702385

23712386
await new Promise<void>((resolve, reject) => {
2372-
buildStream.on('end', () => {
2373-
CORE_LOGGER.debug(`Image '${job.containerImage}' built successfully.`)
2374-
this.updateImageUsage(job.containerImage).catch((e) => {
2375-
CORE_LOGGER.debug(`Failed to track image usage: ${e.message}`)
2387+
let settled = false
2388+
const detachBuildLog = () => {
2389+
buildStream.removeListener('data', onBuildData)
2390+
}
2391+
const finish = (action: () => void) => {
2392+
if (settled) return
2393+
settled = true
2394+
action()
2395+
}
2396+
const onAbort = () => {
2397+
finish(() => {
2398+
detachBuildLog()
2399+
buildStream.destroy()
2400+
const err = new Error('Image build aborted') as NodeJS.ErrnoException
2401+
err.code = 'ABORT_ERR'
2402+
err.name = 'AbortError'
2403+
reject(err)
23762404
})
2377-
resolve()
2378-
})
2405+
}
2406+
controller.signal.addEventListener('abort', onAbort, { once: true })
2407+
const onSuccess = () => {
2408+
finish(() => {
2409+
detachBuildLog()
2410+
controller.signal.removeEventListener('abort', onAbort)
2411+
CORE_LOGGER.debug(`Image '${job.containerImage}' built successfully.`)
2412+
this.updateImageUsage(job.containerImage).catch((e) => {
2413+
CORE_LOGGER.debug(`Failed to track image usage: ${e.message}`)
2414+
})
2415+
resolve()
2416+
})
2417+
}
2418+
// Some HTTP responses emit `close` without a reliable `end`; handle both (settled ensures once).
2419+
buildStream.on('end', onSuccess)
2420+
buildStream.on('close', onSuccess)
23792421
buildStream.on('error', (err) => {
23802422
CORE_LOGGER.debug(`Error building image '${job.containerImage}':` + err.message)
23812423
appendFileSync(imageLogFile, String(err.message))
2382-
reject(err)
2424+
finish(() => {
2425+
detachBuildLog()
2426+
controller.signal.removeEventListener('abort', onAbort)
2427+
reject(err)
2428+
})
23832429
})
23842430
})
23852431
job.status = C2DStatusNumber.ConfiguringVolumes
23862432
job.statusText = C2DStatusText.ConfiguringVolumes
2387-
this.db.updateJob(job)
2433+
await this.db.updateJob(job)
23882434
} catch (err) {
2389-
CORE_LOGGER.error(
2390-
`Unable to build docker image: ${job.containerImage}: ${err.message}`
2391-
)
2392-
appendFileSync(imageLogFile, String(err.message))
2435+
const aborted =
2436+
(err as NodeJS.ErrnoException)?.code === 'ABORT_ERR' ||
2437+
(err as Error)?.name === 'AbortError'
2438+
if (aborted) {
2439+
// timeout-specific handling
2440+
const msg = `Image build timed out after ${timeoutMs / 1000}s`
2441+
CORE_LOGGER.error(`Unable to build docker image: ${job.containerImage}: ${msg}`)
2442+
appendFileSync(imageLogFile, msg)
2443+
} else {
2444+
CORE_LOGGER.error(
2445+
`Unable to build docker image: ${job.containerImage}: ${err.message}`
2446+
)
2447+
appendFileSync(imageLogFile, String(err.message))
2448+
}
23932449
job.status = C2DStatusNumber.BuildImageFailed
23942450
job.statusText = C2DStatusText.BuildImageFailed
23952451
job.isRunning = false
23962452
job.dateFinished = String(Date.now() / 1000)
23972453
await this.db.updateJob(job)
23982454
await this.cleanupJob(job)
2455+
} finally {
2456+
clearTimeout(timer)
23992457
}
24002458
}
24012459

0 commit comments

Comments
 (0)