balena-cli/lib/utils/deploy-legacy.js

213 lines
5.1 KiB
JavaScript
Raw Normal View History

/**
* @license
* Copyright 2020 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 * as Promise from 'bluebird';
import { getVisuals } from './lazy';
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}`;
};
const bufferImage = function(docker, imageId, bufferFile) {
const streamUtils = require('./streams');
const image = docker.getImage(imageId);
const imageMetadata = image.inspect();
return Promise.join(
image.get(),
imageMetadata.get('Size'),
(imageStream, imageSize) =>
streamUtils.buffer(imageStream, bufferFile).tap(bufferedStream => {
// @ts-ignore adding an extra property
bufferedStream.length = imageSize;
}),
);
};
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),
});
};
/*
opts must be a hash with the following keys:
- appName: the name of the app to deploy to
- imageName: the name of the image to deploy
- buildLogs: a string with build output
- shouldUploadLogs
*/
export const deployLegacy = function(
docker,
logger,
token,
username,
url,
opts,
) {
const tmp = require('tmp');
const tmpNameAsync = Promise.promisify(tmp.tmpName);
// Ensure the tmp files gets deleted
tmp.setGracefulCleanup();
const { appName, imageName, buildLogs, shouldUploadLogs } = opts;
const logs = buildLogs;
return tmpNameAsync()
.then(function(bufferFile) {
logger.logInfo('Initializing deploy...');
return 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
Promise.try(() => require('mz/fs').unlink(bufferFile)).catchReturn(
undefined,
),
);
})
.tap(function({ buildId }) {
if (!shouldUploadLogs) {
return;
}
logger.logInfo('Uploading logs...');
return Promise.join(
logs,
token,
url,
buildId,
username,
appName,
uploadLogs,
);
})
.get('buildId');
};