/** * @license * Copyright 2017-2021 Balena Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { getVisuals } from './lazy'; import { promisify } from 'util'; const getBuilderPushEndpoint = function (baseUrl, owner, app) { const querystring = require('querystring'); const args = querystring.stringify({ owner, app }); return `https://builder.${baseUrl}/v1/push?${args}`; }; const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) { const querystring = require('querystring'); const args = querystring.stringify({ owner, app, buildId }); return `https://builder.${baseUrl}/v1/pushLogs?${args}`; }; /** * @param {import('dockerode')} docker * @param {string} imageId * @param {string} bufferFile */ const bufferImage = function (docker, imageId, bufferFile) { const streamUtils = require('./streams'); const image = docker.getImage(imageId); const sizePromise = image.inspect().then((img) => img.Size); return Promise.all([image.get(), sizePromise]).then( ([imageStream, imageSize]) => streamUtils.buffer(imageStream, bufferFile).then((bufferedStream) => { // @ts-ignore adding an extra property bufferedStream.length = imageSize; return bufferedStream; }), ); }; const showPushProgress = function (message) { const visuals = getVisuals(); const progressBar = new visuals.Progress(message); progressBar.update({ percentage: 0 }); return progressBar; }; const uploadToPromise = (uploadRequest, logger) => new Promise(function (resolve, reject) { const handleMessage = function (data) { let obj; data = data.toString(); logger.logDebug(`Received data: ${data}`); try { obj = JSON.parse(data); } catch (e) { logger.logError('Error parsing reply from remote side'); reject(e); return; } switch (obj.type) { case 'error': reject(new Error(`Remote error: ${obj.error}`)); break; case 'success': resolve(obj); break; case 'status': logger.logInfo(obj.message); break; default: reject(new Error(`Received unexpected reply from remote: ${data}`)); } }; uploadRequest.on('error', reject).on('data', handleMessage); }); /** * @returns {Promise<{ buildId: number }>} */ const uploadImage = function ( imageStream, token, username, url, appName, logger, ) { const request = require('request'); const progressStream = require('progress-stream'); const zlib = require('zlib'); // Need to strip off the newline const progressMessage = logger .formatMessage('info', 'Uploading') .slice(0, -1); const progressBar = showPushProgress(progressMessage); const streamWithProgress = imageStream.pipe( progressStream( { time: 500, length: imageStream.length, }, ({ percentage, eta }) => progressBar.update({ percentage: Math.min(percentage, 100), eta, }), ), ); const uploadRequest = request.post({ url: getBuilderPushEndpoint(url, username, appName), headers: { 'Content-Encoding': 'gzip', }, auth: { bearer: token, }, body: streamWithProgress.pipe( zlib.createGzip({ level: 6, }), ), }); return uploadToPromise(uploadRequest, logger); }; const uploadLogs = function (logs, token, url, buildId, username, appName) { const request = require('request'); return request.post({ json: true, url: getBuilderLogPushEndpoint(url, buildId, username, appName), auth: { bearer: token, }, body: Buffer.from(logs), }); }; /** * @param {import('dockerode')} docker * @param {import('./logger')} logger * @param {string} token * @param {string} username * @param {string} url * @param {{appName: string; imageName: string; buildLogs: string; shouldUploadLogs: boolean}} opts * - appName: the name of the app to deploy to * - imageName: the name of the image to deploy * - buildLogs: a string with build output */ export const deployLegacy = async function ( docker, logger, token, username, url, opts, ) { const tmp = require('tmp'); const tmpNameAsync = promisify(tmp.tmpName); // Ensure the tmp files gets deleted tmp.setGracefulCleanup(); const { appName, imageName, buildLogs, shouldUploadLogs } = opts; const logs = buildLogs; const bufferFile = await tmpNameAsync(); logger.logInfo('Initializing deploy...'); const { buildId } = await bufferImage(docker, imageName, bufferFile) .then((stream) => uploadImage(stream, token, username, url, appName, logger), ) .finally(() => // If the file was never written to (for instance because an error // has occured before any data was written) this call will throw an // ugly error, just suppress it require('fs') .promises.unlink(bufferFile) .catch(() => undefined), ); if (shouldUploadLogs) { logger.logInfo('Uploading logs...'); const args = await Promise.all([ logs, token, url, buildId, username, appName, ]); await uploadLogs(...args); } return buildId; };