UNPKG

balena-cli

Version:

The official balena Command Line Interface

409 lines • 15.6 kB
"use strict"; 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