diff --git a/CHANGES b/CHANGES index 5e13babba..bfa4dd00b 100644 --- a/CHANGES +++ b/CHANGES @@ -3208,4 +3208,5 @@ [9.1.6] * apps: fix wrong disabled state for devices config * notifications: send email when manual platform and app update required +* source install: support dockerfileName and build options diff --git a/src/apps.js b/src/apps.js index d412ced96..eba948287 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1921,9 +1921,14 @@ async function install(data, auditSource) { if (data.sourceArchiveFilePath) await fileUtils.renameFile(data.sourceArchiveFilePath, `${paths.SOURCE_ARCHIVES_DIR}/${appId}.tar.gz`); + const buildConfig = { + buildArgs: data.buildArgs || [], + dockerfileName: data.dockerfileName || null + }; + const task = { - args: { restoreConfig: null, skipDnsSetup, overwriteDns }, - values: { }, + args: { restoreConfig: null, skipDnsSetup, overwriteDns, buildConfig }, + values: {}, requiredState: app.installationState }; @@ -2340,6 +2345,7 @@ async function updateApp(app, data, auditSource) { const updateConfig = { skipBackup, manifest }; // this will clear appStoreId/versionsUrl when updating from a repo and set it if passed in for update route if ('appStoreId' in data) updateConfig.appStoreId = data.appStoreId; if ('versionsUrl' in data) updateConfig.versionsUrl = data.versionsUrl; + updateConfig.buildConfig = { buildArgs: data.buildArgs || [], dockerfileName: data.dockerfileName || null }; // prevent user from installing a app with different manifest id over an existing app // this allows cloudron install -f --app for an app installed from the appStore diff --git a/src/apptask.js b/src/apptask.js index 99ae7ff34..318c6a7b0 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -195,10 +195,9 @@ async function downloadImage(manifest) { await docker.downloadImage(manifest); } -async function buildLocalImage(app) { +async function buildLocalImage(app, buildConfig) { assert.strictEqual(typeof app, 'object'); - - // TODO some precondition checks like downloadImage maybe + assert.strictEqual(typeof buildConfig, 'object'); const sourceFilePath = path.join(paths.APPS_DATA_DIR, app.id, 'source.tar.gz'); @@ -220,7 +219,7 @@ async function buildLocalImage(app) { } } - await docker.buildImage(app.manifest.dockerImage, sourceFilePath); + await docker.buildImage(app.manifest.dockerImage, sourceFilePath, buildConfig); } async function updateChecklist(app, newChecks, acknowledged = false) { @@ -484,7 +483,7 @@ async function installCommand(app, args, progressCallback) { // now we have the local package tarball, so lets build if (app.manifest.dockerImage.indexOf('local/') === 0) { await progressCallback({ percent: 75, message: 'Building image' }); - await buildLocalImage(app); + await buildLocalImage(app, args.buildConfig); } await progressCallback({ percent: 80, message: 'Creating container' }); @@ -785,7 +784,7 @@ async function updateCommand(app, args, progressCallback) { // now we have the local package tarball, so lets build if (app.manifest.dockerImage.indexOf('local/') === 0) { await progressCallback({ percent: 65, message: 'Building image' }); - await buildLocalImage(app); + await buildLocalImage(app, updateConfig.buildConfig); } await progressCallback({ percent: 70, message: 'Creating container' }); diff --git a/src/docker.js b/src/docker.js index 3634355ce..d2332f4f1 100644 --- a/src/docker.js +++ b/src/docker.js @@ -131,25 +131,43 @@ async function pullImage(imageRef) { }); } -async function buildImage(dockerImage, sourceArchiveFilePath) { +async function buildImage(dockerImage, sourceArchiveFilePath, buildConfig) { assert.strictEqual(typeof dockerImage, 'string'); assert.strictEqual(typeof sourceArchiveFilePath, 'string'); + assert.strictEqual(typeof buildConfig, 'object'); log(`buildImage: building ${dockerImage} from ${sourceArchiveFilePath}`); const buildOptions = { t: dockerImage }; - const [listError, listOut] = await safe(shell.spawn('tar', ['-tzf', sourceArchiveFilePath], { encoding: 'utf8' })); - if (!listError && listOut) { - const dockerfileCloudronPath = listOut.split('\n').map(line => line.trim()).find(line => { - const path = line.replace(/\/$/, ''); - return path.endsWith('Dockerfile.cloudron'); - }); - if (dockerfileCloudronPath) { - buildOptions.dockerfile = dockerfileCloudronPath.replace(/\/$/, ''); - log(`buildImage: using ${buildOptions.dockerfile}`); + + if (buildConfig.dockerfileName) { + buildOptions.dockerfile = buildConfig.dockerfileName; + log(`buildImage: using ${buildOptions.dockerfile}`); + } else { + const [listError, listOut] = await safe(shell.spawn('tar', ['-tzf', sourceArchiveFilePath], { encoding: 'utf8' })); + if (!listError && listOut) { + const dockerfileCloudronPath = listOut.split('\n').map(line => line.trim()).find(line => { + const p = line.replace(/\/$/, ''); + return p.endsWith('Dockerfile.cloudron'); + }); + if (dockerfileCloudronPath) { + buildOptions.dockerfile = dockerfileCloudronPath.replace(/\/$/, ''); + log(`buildImage: using ${buildOptions.dockerfile}`); + } } } + if (buildConfig.buildArgs.length > 0) { + buildOptions.buildargs = {}; + for (const arg of buildConfig.buildArgs) { + const eqIndex = arg.indexOf('='); + if (eqIndex !== -1) { + buildOptions.buildargs[arg.slice(0, eqIndex)] = arg.slice(eqIndex + 1); + } + } + log(`buildImage: using build args ${Object.keys(buildOptions.buildargs).join(', ')}`); + } + const tarStream = fs.createReadStream(sourceArchiveFilePath); const [error, stream] = await safe(gConnection.buildImage(tarStream, buildOptions)); if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to build image from ${sourceArchiveFilePath}: ${error.message}`); diff --git a/src/routes/apps.js b/src/routes/apps.js index bf18fbf98..2acac55ef 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -131,6 +131,12 @@ async function install(req, res, next) { if ('cpuQuota' in data && data.cpuQuota !== 'number') return next(new HttpError(400, 'cpuQuota is not a number')); if ('operators' in data && typeof data.operators !== 'object') return next(new HttpError(400, 'operators must be an object')); + if ('buildArgs' in data) { + if (typeof data.buildArgs === 'string') data.buildArgs = safe.JSON.parse(data.buildArgs); + if (!Array.isArray(data.buildArgs)) return next(new HttpError(400, 'buildArgs must be an array')); + } + if ('dockerfileName' in data && typeof data.dockerfileName !== 'string') return next(new HttpError(400, 'dockerfileName must be a string')); + let error, result; if (data.versionsUrl) { [error, result] = await safe(community.downloadManifest(data.versionsUrl)); @@ -642,6 +648,12 @@ async function update(req, res, next) { if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean')); if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean')); + if ('buildArgs' in data) { + if (typeof data.buildArgs === 'string') data.buildArgs = safe.JSON.parse(data.buildArgs); + if (!Array.isArray(data.buildArgs)) return next(new HttpError(400, 'buildArgs must be an array')); + } + if ('dockerfileName' in data && typeof data.dockerfileName !== 'string') return next(new HttpError(400, 'dockerfileName must be a string')); + let error, result; if (data.versionsUrl) { [error, result] = await safe(community.downloadManifest(data.versionsUrl));