balena-cli
Version:
The official balena Command Line Interface
311 lines • 14.2 kB
JavaScript
"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