UNPKG

balena-cli

Version:

The official balena Command Line Interface

311 lines • 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LivepushManager = void 0; const chokidar = require("chokidar"); const fs = require("fs"); const livepush_1 = require("livepush"); const _ = require("lodash"); const path = require("path"); const errors_1 = require("../../errors"); const livepush_2 = require("livepush"); const deploy_1 = require("./deploy"); const errors_2 = require("./errors"); const logs_1 = require("./logs"); const helpers_1 = require("../helpers"); const DEVICE_STATUS_SETTLE_CHECK_INTERVAL = 1000; const LIVEPUSH_DEBOUNCE_TIMEOUT = 2000; class LivepushManager { constructor(opts) { this.lastDeviceStatus = null; this.containers = {}; this.dockerfilePaths = {}; this.updateEventsWaiting = {}; this.deleteEventsWaiting = {}; this.rebuildsRunning = {}; this.rebuildRunningIds = {}; this.rebuildsCancelled = {}; this.getDebouncedEventHandler = _.memoize((serviceName) => { return _.debounce(() => this.handleFSEvents(serviceName), LIVEPUSH_DEBOUNCE_TIMEOUT); }); this.buildContext = opts.buildContext; this.composition = opts.composition; this.buildTasks = opts.buildTasks; this.docker = opts.docker; this.api = opts.api; this.logger = opts.logger; this.deployOpts = opts.deployOpts; this.imageIds = opts.imageIds; } async init() { this.deviceInfo = await this.api.getDeviceInformation(); this.logger.logLivepush('Waiting for device state to settle...'); await this.awaitDeviceStateSettle(); this.logger.logLivepush('Device state settled'); const { getDockerignoreByService } = await Promise.resolve().then(() => require('../ignore')); const { getServiceDirsFromComposition } = await Promise.resolve().then(() => require('../compose_ts')); const rootContext = path.resolve(this.buildContext); const serviceDirsByService = await getServiceDirsFromComposition(this.deployOpts.source, this.composition); const dockerignoreByService = await getDockerignoreByService(this.deployOpts.source, this.deployOpts.multiDockerignore, serviceDirsByService); for (const serviceName of _.keys(this.composition.services)) { const service = this.composition.services[serviceName]; const buildTask = _.find(this.buildTasks, { serviceName }); if (buildTask == null) { throw new Error(`Could not find a build task for service: ${serviceName}`); } if (service.build != null) { if (buildTask.dockerfile == null) { throw new Error(`Could not detect dockerfile for service: ${serviceName}`); } const dockerfile = new livepush_2.Dockerfile(buildTask.dockerfile); if (buildTask.dockerfilePath == null) { this.dockerfilePaths[buildTask.serviceName] = this.getDockerfilePathFromTask(buildTask); } else { this.dockerfilePaths[buildTask.serviceName] = [ buildTask.dockerfilePath, ]; } const container = _.find(this.lastDeviceStatus.containers, { serviceName, }); if (container == null) { return; } const context = path.resolve(rootContext, service.build.context); const livepush = await livepush_1.default.init({ dockerfile, context, containerId: container.containerId, stageImages: this.imageIds[serviceName], docker: this.docker, }); const buildVars = buildTask.buildMetadata.getBuildVarsForService(buildTask.serviceName); if (!_.isEmpty(buildVars)) { livepush.setBuildArgs(buildVars); } this.assignLivepushOutputHandlers(serviceName, livepush); this.updateEventsWaiting[serviceName] = []; this.deleteEventsWaiting[serviceName] = []; const addEvent = ($serviceName, changedPath) => { this.logger.logDebug(`Got an add filesystem event for service: ${$serviceName}. File: ${changedPath}`); const eventQueue = this.updateEventsWaiting[$serviceName]; eventQueue.push(changedPath); this.getDebouncedEventHandler($serviceName)(); }; const monitor = this.setupFilesystemWatcher(serviceName, rootContext, context, addEvent, dockerignoreByService, this.deployOpts.multiDockerignore); this.containers[serviceName] = { livepush, context, monitor, containerId: container.containerId, }; this.rebuildsRunning[serviceName] = false; this.rebuildsCancelled[serviceName] = false; } } } async cleanup() { this.logger.logLivepush('Cleaning up device...'); await Promise.all(_.map(this.containers, (container) => container.livepush.cleanupIntermediateContainers())); this.logger.logDebug('Cleaning up done.'); } setupFilesystemWatcher(serviceName, rootContext, serviceContext, changedPathHandler, dockerignoreByService, multiDockerignore) { const contextForDockerignore = multiDockerignore ? serviceContext : rootContext; const dockerignore = dockerignoreByService[serviceName]; const monitor = chokidar.watch('.', { cwd: serviceContext, followSymlinks: true, ignoreInitial: true, ignored: (filePath, stats) => { if (!stats) { try { stats = fs.lstatSync(filePath); } catch (err) { } } if (stats && !stats.isFile() && !stats.isSymbolicLink()) { return !stats.isDirectory(); } const relPath = path.relative(contextForDockerignore, filePath); return dockerignore.ignores(relPath); }, }); monitor.on('add', (changedPath) => changedPathHandler(serviceName, changedPath)); monitor.on('change', (changedPath) => changedPathHandler(serviceName, changedPath)); monitor.on('unlink', (changedPath) => changedPathHandler(serviceName, changedPath)); return monitor; } close() { for (const container of Object.values(this.containers)) { container.monitor.close().catch((err) => { if (process.env.DEBUG) { this.logger.logDebug(`chokidar.close() ${err.message}`); } }); } } static preprocessDockerfile(content) { return new livepush_2.Dockerfile(content).generateLiveDockerfile(); } async awaitDeviceStateSettle() { this.lastDeviceStatus = await this.api.getStatus(); if (this.lastDeviceStatus.appState === 'applied') { return; } this.logger.logDebug(`Device state not settled, retrying in ${DEVICE_STATUS_SETTLE_CHECK_INTERVAL}ms`); await (0, helpers_1.delay)(DEVICE_STATUS_SETTLE_CHECK_INTERVAL); await this.awaitDeviceStateSettle(); } async handleFSEvents(serviceName) { const updated = this.updateEventsWaiting[serviceName]; const deleted = this.deleteEventsWaiting[serviceName]; this.updateEventsWaiting[serviceName] = []; this.deleteEventsWaiting[serviceName] = []; if (_.some(this.dockerfilePaths[serviceName], (name) => _.some(updated, (changed) => name === changed))) { this.logger.logLivepush(`Detected Dockerfile change, performing full rebuild of service ${serviceName}`); await this.handleServiceRebuild(serviceName); return; } const livepush = this.containers[serviceName].livepush; if (!livepush.livepushNeeded(updated, deleted)) { return; } this.logger.logLivepush(`Detected changes for container ${serviceName}, updating...`); try { await livepush.performLivepush(updated, deleted); } catch (e) { this.logger.logError(`An error occured whilst trying to perform a livepush: `); if ((0, errors_1.instanceOf)(e, livepush_1.ContainerNotRunningError)) { this.logger.logError(' Livepush container not running'); } else { this.logger.logError(` ${e.message}`); } this.logger.logDebug(e.stack); } } async handleServiceRebuild(serviceName) { if (this.rebuildsRunning[serviceName]) { this.logger.logLivepush(`Cancelling ongoing rebuild for service ${serviceName}`); await this.cancelRebuild(serviceName); while (this.rebuildsCancelled[serviceName]) { await (0, helpers_1.delay)(1000); } } this.rebuildsRunning[serviceName] = true; try { const buildTask = _.find(this.buildTasks, { serviceName }); if (buildTask == null) { throw new Error(`Could not find a build task for service ${serviceName}`); } let stageImages; try { stageImages = await (0, deploy_1.rebuildSingleTask)(serviceName, this.docker, this.logger, this.deviceInfo, this.composition, this.buildContext, this.deployOpts, (id) => { this.rebuildRunningIds[serviceName] = id; }); } catch (e) { if (!(e instanceof errors_2.BuildError)) { throw e; } if (this.rebuildsCancelled[serviceName]) { return; } this.logger.logError(`Rebuild of service ${serviceName} failed!\n Error: ${e.getServiceError(serviceName)}`); return; } finally { delete this.rebuildRunningIds[serviceName]; } if (this.rebuildsCancelled[serviceName]) { return; } const containerId = await this.api.getContainerId(serviceName); await this.docker.getContainer(containerId).remove({ force: true }); const currentState = await this.api.getTargetState(); await this.api.setTargetState((0, deploy_1.generateTargetState)(currentState, this.composition, this.buildTasks, {})); await this.awaitDeviceStateSettle(); const instance = this.containers[serviceName]; const container = _.find(this.lastDeviceStatus.containers, { serviceName, }); if (container == null) { throw new Error(`Could not find new container for service ${serviceName}`); } const dockerfile = new livepush_2.Dockerfile(buildTask.dockerfile); instance.livepush = await livepush_1.default.init({ dockerfile, context: buildTask.context, containerId: container.containerId, stageImages, docker: this.docker, }); this.assignLivepushOutputHandlers(serviceName, instance.livepush); } catch (e) { this.logger.logError(`There was an error rebuilding the service: ${e}`); } finally { this.rebuildsRunning[serviceName] = false; this.rebuildsCancelled[serviceName] = false; } } async cancelRebuild(serviceName) { this.rebuildsCancelled[serviceName] = true; if (this.rebuildRunningIds[serviceName] != null) { try { await this.docker .getContainer(this.rebuildRunningIds[serviceName]) .remove({ force: true }); await this.containers[serviceName].livepush.cancel(); } catch (_a) { } } } assignLivepushOutputHandlers(serviceName, livepush) { const msgString = (msg) => `[${(0, logs_1.getServiceColourFn)(serviceName)(serviceName)}] ${msg}`; const log = (msg) => this.logger.logLivepush(msgString(msg)); const error = (msg) => this.logger.logError(msgString(msg)); const debugLog = (msg) => this.logger.logDebug(msgString(msg)); livepush.on('commandExecute', (command) => log(`Executing command: \`${command.command}\``)); livepush.on('commandOutput', (output) => log(` ${output.output.data.toString()}`)); livepush.on('commandReturn', ({ returnCode, command }) => { if (returnCode !== 0) { error(` Command ${command} failed with exit code: ${returnCode}`); } else { debugLog(`Command ${command} exited successfully`); } }); livepush.on('containerRestart', () => { log('Restarting service...'); }); livepush.on('cancel', () => { log('Cancelling current livepush...'); }); } getDockerfilePathFromTask(task) { switch (task.projectType) { case 'Standard Dockerfile': return ['Dockerfile']; case 'Dockerfile.template': return ['Dockerfile.template']; case 'Architecture-specific Dockerfile': return [ `Dockerfile.${this.deviceInfo.arch}`, `Dockerfile.${this.deviceInfo.deviceType}`, ]; default: return []; } } } exports.LivepushManager = LivepushManager; exports.default = LivepushManager; //# sourceMappingURL=live.js.map