balena-cli
Version:
The official balena Command Line Interface
409 lines • 15.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.deployToDevice = deployToDevice;
exports.rebuildSingleTask = rebuildSingleTask;
exports.generateTargetState = generateTargetState;
const semver = require("balena-semver");
const Docker = require("dockerode");
const _ = require("lodash");
const multibuild_1 = require("@balena/compose/dist/multibuild");
const config_1 = require("../../config");
const errors_1 = require("../../errors");
const compose_ts_1 = require("../compose_ts");
const Logger = require("../logger");
const api_1 = require("./api");
const LocalPushErrors = require("./errors");
const live_1 = require("./live");
const logs_1 = require("./logs");
const lazy_1 = require("../lazy");
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = '10ca12e1ea5e';
const LOCAL_PROJECT_NAME = 'local_image';
const globalLogger = Logger.getLogger();
async function environmentFromInput(envs, serviceNames, logger) {
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
const ret = {};
for (const service of serviceNames) {
ret[service] = {};
}
for (const env of envs) {
const maybeMatch = env.match(varRegex);
if (maybeMatch == null) {
throw new errors_1.ExpectedError(`Unable to parse environment variable: ${env}`);
}
const match = maybeMatch;
let service;
if (match[1]) {
if (!(match[1] in ret)) {
logger.logDebug(`Warning: Cannot find a service with name ${match[1]}. Treating the string as part of the environment variable name.`);
match[2] = `${match[1]}:${match[2]}`;
}
else {
service = match[1];
}
}
if (service != null) {
ret[service][match[2]] = match[3];
}
else {
for (const serviceName of serviceNames) {
ret[serviceName][match[2]] = match[3];
}
}
}
return ret;
}
async function deployToDevice(opts) {
if (opts.deviceHost.includes('.local')) {
const util = await Promise.resolve().then(() => require('util'));
const dns = await Promise.resolve().then(() => require('dns'));
const { address } = await util.promisify(dns.lookup)(opts.deviceHost, {
family: 4,
});
opts.deviceHost = address;
}
const port = 48484;
const api = new api_1.DeviceAPI(globalLogger, opts.deviceHost, port);
try {
globalLogger.logDebug('Checking we can access device');
await api.ping();
}
catch (e) {
throw new errors_1.ExpectedError((0, lazy_1.stripIndent) `
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
Device may not have local mode enabled. Check with:
balena device local-mode <device-uuid>
`);
}
const versionError = new Error('The supervisor version on this remote device does not support multicontainer local mode. ' +
'Please update your device to balenaOS v2.20.0 or greater from the dashboard.');
try {
const version = await api.getVersion();
globalLogger.logDebug(`Checking device supervisor version: ${version}`);
if (!semver.satisfies(version, '>=7.21.4')) {
throw new errors_1.ExpectedError(versionError);
}
if (!opts.nolive && !semver.satisfies(version, '>=9.7.0')) {
globalLogger.logWarn(`Using livepush requires a balena supervisor version >= 9.7.0. A live session will not be started.`);
opts.nolive = true;
}
}
catch (e) {
if (e instanceof LocalPushErrors.DeviceAPIError) {
throw new errors_1.ExpectedError(versionError);
}
else {
throw e;
}
}
globalLogger.logInfo(`Starting build on device ${opts.deviceHost}`);
const project = await (0, compose_ts_1.loadProject)(globalLogger, {
convertEol: opts.convertEol,
dockerfilePath: opts.dockerfilePath,
multiDockerignore: opts.multiDockerignore,
noParentCheck: opts.noParentCheck,
projectName: 'local',
projectPath: opts.source,
isLocal: true,
});
const docker = connectToDocker(opts.deviceHost, opts.devicePort != null ? opts.devicePort : 2375);
await (0, compose_ts_1.checkBuildSecretsRequirements)(docker, opts.source);
globalLogger.logDebug('Tarring all non-ignored files...');
const tarStartTime = Date.now();
const tarStream = await (0, compose_ts_1.tarDirectory)(opts.source, {
composition: project.composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
});
globalLogger.logDebug(`Tarring complete in ${Date.now() - tarStartTime} ms`);
globalLogger.logDebug('Fetching device information...');
const deviceInfo = await api.getDeviceInformation();
let imageIds;
if (!opts.nolive) {
imageIds = {};
}
const { awaitInterruptibleTask } = await Promise.resolve().then(() => require('../helpers'));
const buildTasks = await awaitInterruptibleTask(performBuilds, project.composition, tarStream, docker, deviceInfo, globalLogger, opts, imageIds);
globalLogger.outputDeferredMessages();
console.log();
const envs = await environmentFromInput(opts.env, Object.getOwnPropertyNames(project.composition.services), globalLogger);
globalLogger.logDebug('Setting device state...');
const currentTargetState = await api.getTargetState();
const targetState = generateTargetState(currentTargetState, project.composition, buildTasks, envs);
globalLogger.logDebug(`Sending target state: ${JSON.stringify(targetState)}`);
await api.setTargetState(targetState);
const promises = [streamDeviceLogs(api, opts)];
let livepush = null;
if (!opts.nolive) {
livepush = new live_1.default({
api,
buildContext: opts.source,
buildTasks,
docker,
logger: globalLogger,
composition: project.composition,
imageIds: imageIds,
deployOpts: opts,
});
promises.push(livepush.init());
if (opts.detached) {
globalLogger.logLivepush('Running in detached mode, no service logs will be shown');
}
globalLogger.logLivepush('Watching for file changes...');
}
try {
await awaitInterruptibleTask(() => Promise.all(promises));
}
finally {
livepush === null || livepush === void 0 ? void 0 : livepush.close();
await (livepush === null || livepush === void 0 ? void 0 : livepush.cleanup());
}
}
async function streamDeviceLogs(deviceApi, opts) {
if (opts.detached) {
return;
}
globalLogger.logInfo('Streaming device logs...');
const { connectAndDisplayDeviceLogs } = await Promise.resolve().then(() => require('./logs'));
return connectAndDisplayDeviceLogs({
deviceApi,
logger: globalLogger,
system: opts.system || false,
filterServices: opts.services,
maxAttempts: 1001,
});
}
function connectToDocker(host, port) {
return new Docker({
host,
port,
});
}
function extractDockerArrowMessage(outputLine) {
const arrowTest = /^.*\s*-+>\s*(.+)/i;
const match = arrowTest.exec(outputLine);
if (match != null) {
return match[1];
}
}
async function performBuilds(composition, tarStream, docker, deviceInfo, logger, opts, imageIds) {
const multibuild = await Promise.resolve().then(() => require('@balena/compose/dist/multibuild'));
const buildTasks = await (0, compose_ts_1.makeBuildTasks)(composition, tarStream, deviceInfo, logger, LOCAL_APPNAME, LOCAL_RELEASEHASH, (content) => {
if (!opts.nolive) {
return live_1.default.preprocessDockerfile(content);
}
else {
return content;
}
});
logger.logDebug('Probing remote daemon for cache images');
await assignDockerBuildOpts(docker, buildTasks, opts);
let logHandlers;
const lastArrowMessage = {};
if (imageIds != null) {
for (const task of buildTasks) {
if (!task.external) {
imageIds[task.serviceName] = [];
}
}
logHandlers = (serviceName, line) => {
if (/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage[serviceName] != null) {
imageIds[serviceName].push(lastArrowMessage[serviceName]);
}
else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage[serviceName] = msg;
}
}
};
}
logger.logDebug('Starting builds...');
assignOutputHandlers(buildTasks, logger, logHandlers);
const localImages = await multibuild.performBuilds(buildTasks, docker, config_1.BALENA_ENGINE_TMP_PATH);
await inspectBuildResults(localImages);
const imagesToRemove = [];
await Promise.all(localImages.map(async (localImage) => {
if (localImage.external) {
const image = docker.getImage(localImage.name);
await image.tag({
repo: (0, compose_ts_1.makeImageName)(LOCAL_PROJECT_NAME, localImage.serviceName, 'latest'),
force: true,
});
imagesToRemove.push(localImage.name);
}
}));
await Promise.all(_.uniq(imagesToRemove).map((image) => docker.getImage(image).remove({ force: true })));
return buildTasks;
}
async function rebuildSingleTask(serviceName, docker, logger, deviceInfo, composition, source, opts, containerIdCb) {
const multibuild = await Promise.resolve().then(() => require('@balena/compose/dist/multibuild'));
const stageIds = [];
let lastArrowMessage;
const logHandler = (_s, line) => {
if (/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage != null) {
stageIds.push(lastArrowMessage);
}
else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage = msg;
}
}
if (containerIdCb != null) {
const match = line.match(/^\s*--->\s*Running\s*in\s*([a-f0-9]*)\s*$/i);
if (match != null) {
containerIdCb(match[1]);
}
}
};
const tarStream = await (0, compose_ts_1.tarDirectory)(source, {
composition,
convertEol: opts.convertEol,
multiDockerignore: opts.multiDockerignore,
});
const task = _.find(await (0, compose_ts_1.makeBuildTasks)(composition, tarStream, deviceInfo, logger, LOCAL_APPNAME, LOCAL_RELEASEHASH, (content) => {
if (!opts.nolive) {
return live_1.default.preprocessDockerfile(content);
}
else {
return content;
}
}), { serviceName });
if (task == null) {
throw new errors_1.ExpectedError(`Could not find build task for service ${serviceName}`);
}
await assignDockerBuildOpts(docker, [task], opts);
await assignOutputHandlers([task], logger, logHandler);
const [localImage] = await multibuild.performBuilds([task], docker, config_1.BALENA_ENGINE_TMP_PATH);
if (!localImage.successful) {
throw new LocalPushErrors.BuildError([
{
error: localImage.error,
serviceName,
},
]);
}
return stageIds;
}
function assignOutputHandlers(buildTasks, logger, logCb) {
_.each(buildTasks, (task) => {
if (task.external) {
task.progressHook = (progressObj) => {
(0, logs_1.displayBuildLog)({ serviceName: task.serviceName, message: progressObj.progress }, logger);
};
}
else {
task.streamHook = (stream) => {
stream.on('data', (buf) => {
const str = _.trimEnd(buf.toString());
if (str !== '') {
(0, logs_1.displayBuildLog)({ serviceName: task.serviceName, message: str }, logger);
if (logCb) {
logCb(task.serviceName, str);
}
}
});
};
}
});
}
async function getDeviceDockerImages(docker) {
const images = await docker.listImages({ all: true });
return _.map(images, 'Id');
}
async function assignDockerBuildOpts(docker, buildTasks, opts) {
const images = await getDeviceDockerImages(docker);
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
await Promise.all(buildTasks.map(async (task) => {
task.dockerOpts = {
...(task.dockerOpts || {}),
...{
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,
forcerm: true,
pull: opts.pull,
},
t: getImageNameFromTask(task),
nocache: opts.nocache,
forcerm: true,
pull: opts.pull,
};
if (task.external) {
task.dockerOpts.authconfig = (0, multibuild_1.getAuthConfigObj)(task.imageName, opts.registrySecrets);
}
else {
task.dockerOpts.registryconfig = opts.registrySecrets;
}
}));
}
function getImageNameFromTask(task) {
return !task.external && task.tag
? task.tag
: (0, compose_ts_1.makeImageName)(LOCAL_PROJECT_NAME, task.serviceName, 'latest');
}
function generateTargetState(currentTargetState, composition, buildTasks, env) {
const keyedBuildTasks = _.keyBy(buildTasks, 'serviceName');
const services = {};
let idx = 1;
_.each(composition.services, (opts, name) => {
opts = _.cloneDeep(opts);
delete opts.build;
delete opts.image;
const defaults = {
environment: {},
labels: {},
};
opts.environment = _.merge(opts.environment, env[name]);
const contract = keyedBuildTasks[name].contract;
const task = keyedBuildTasks[name];
services[idx] = {
...defaults,
...opts,
...(contract != null ? { contract } : {}),
...{
imageId: idx,
serviceName: name,
serviceId: idx,
image: getImageNameFromTask(task),
running: true,
},
};
idx += 1;
});
const targetState = _.cloneDeep(currentTargetState);
delete targetState.local.apps;
targetState.local.apps = {
1: {
name: LOCAL_APPNAME,
commit: LOCAL_RELEASEHASH,
releaseId: '1',
services,
volumes: composition.volumes || {},
networks: composition.networks || {},
},
};
return targetState;
}
async function inspectBuildResults(images) {
const failures = [];
_.each(images, (image) => {
if (!image.successful) {
failures.push({
error: image.error,
serviceName: image.serviceName,
});
}
});
if (failures.length > 0) {
throw new LocalPushErrors.BuildError(failures).toString();
}
}
//# sourceMappingURL=deploy.js.map