diff --git a/src/apptask.js b/src/apptask.js index cc87b4071..08d48aa1b 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -226,7 +226,11 @@ async function downloadImage(manifest) { if (diskUsage.available < (1024*1024*1024)) throw new BoxError(BoxError.DOCKER_ERROR, `Not enough disk space to pull docker image. available: ${diskUsage.available}`); - await docker.downloadImage(manifest); + if (manifest.dockerImage.indexOf('file:') === 0) { + await docker.buildImage(manifest); + } else { + await docker.downloadImage(manifest); + } } async function updateChecklist(app, newChecks, acknowledged = false) { diff --git a/src/docker.js b/src/docker.js index e0419d222..1cf256bd0 100644 --- a/src/docker.js +++ b/src/docker.js @@ -5,6 +5,7 @@ exports = module.exports = { info, df, + buildImage, downloadImage, createContainer, startContainer, @@ -159,6 +160,52 @@ async function pullImage(imageRef) { }); } +async function buildImage(manifest) { + assert.strictEqual(typeof manifest, 'object'); + + const sourceArchivePath = manifest.dockerImage.slice('file:'.length); + const imageTag = `local/${manifest.id}:${manifest.version}`; + + debug(`buildImage: building ${imageTag} from ${sourceArchivePath}`); + + const tarStream = fs.createReadStream(sourceArchivePath); + const [error, stream] = await safe(gConnection.buildImage(tarStream, { t: imageTag })); + if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to build image from ${sourceArchivePath}: ${error.message}`); + + return new Promise((resolve, reject) => { + let buildError = null; + + stream.on('data', (chunk) => { + const data = safe.JSON.parse(chunk) || {}; + + if (data.error) { + buildError = data.errorDetail || { message: data.error }; + } else { + const message = (data.stream || data.status || data.aux?.ID || '').replace(/\n$/, ''); + if (message) debug('buildImage: ' + message); + } + }); + + stream.on('end', () => { + if (buildError) { + debug(`buildImage: error ${buildError}`); + return reject(new BoxError(buildError.message.includes('no space') ? BoxError.FS_ERROR : BoxError.DOCKER_ERROR, buildError.message)); + } else { + debug(`buildImage: success ${imageTag}`); + } + + // TODO we should probably use that scheme directly instead of file:.... + manifest.dockerImage = imageTag; + resolve(); + }); + + stream.on('error', (error) => { + debug(`buildImage: error building image ${imageTag}: %o`, error); + reject(new BoxError(BoxError.DOCKER_ERROR, error.message)); + }); + }); +} + async function downloadImage(manifest) { assert.strictEqual(typeof manifest, 'object'); diff --git a/src/routes/apps.js b/src/routes/apps.js index 987adf399..9485754ac 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -206,6 +206,9 @@ async function install(req, res, next) { data.appStoreId = result.appStoreId; data.manifest = result.manifest; + // if we have a source archive upload, craft a custom docker image URI for later + if (req.files.sourceArchive?.path) data.manifest.dockerImage = 'file:' + req.files.sourceArchive.path; + [error, result] = await safe(apps.install(data, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error));