Make network-manager module a singleton

Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver
2020-06-11 11:05:43 +01:00
committed by Balena CI
parent 3773249790
commit 8fc97b9de8
6 changed files with 249 additions and 353 deletions

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ test/data/led_file
report.xml report.xml
.DS_Store .DS_Store
.tsbuildinfo .tsbuildinfo
.prettierrc

View File

@ -13,7 +13,6 @@ import DeviceState from './device-state';
import { APIBinder } from './api-binder'; import { APIBinder } from './api-binder';
import * as config from './config'; import * as config from './config';
import NetworkManager from './compose/network-manager';
import VolumeManager from './compose/volume-manager'; import VolumeManager from './compose/volume-manager';
import { import {
@ -50,7 +49,6 @@ class ApplicationManager extends EventEmitter {
public services: ServiceManager; public services: ServiceManager;
public volumes: VolumeManager; public volumes: VolumeManager;
public networks: NetworkManager;
public proxyvisor: any; public proxyvisor: any;
public timeSpentFetching: number; public timeSpentFetching: number;

View File

@ -14,17 +14,15 @@ import { docker } from './lib/docker-utils';
import { LocalModeManager } from './local-mode'; import { LocalModeManager } from './local-mode';
import * as updateLock from './lib/update-lock'; import * as updateLock from './lib/update-lock';
import { checkTruthy, checkInt, checkString } from './lib/validation'; import { checkTruthy, checkInt, checkString } from './lib/validation';
import { import { ContractViolationError, InternalInconsistencyError } from './lib/errors';
ContractViolationError,
InternalInconsistencyError,
} from './lib/errors';
import * as dbFormat from './device-state/db-format'; import * as dbFormat from './device-state/db-format';
import { Network } from './compose/network'; import { Network } from './compose/network';
import { ServiceManager } from './compose/service-manager'; import { ServiceManager } from './compose/service-manager';
import * as Images from './compose/images'; import * as Images from './compose/images';
import { NetworkManager } from './compose/network-manager'; import { Network } from './compose/network';
import * as networkManager from './compose/network-manager';
import { VolumeManager } from './compose/volume-manager'; import { VolumeManager } from './compose/volume-manager';
import * as compositionSteps from './compose/composition-steps'; import * as compositionSteps from './compose/composition-steps';
@ -37,7 +35,7 @@ import { serviceAction } from './device-api/common';
import * as db from './db'; import * as db from './db';
// TODO: move this to an Image class? // TODO: move this to an Image class?
const imageForService = (service) => ({ const imageForService = service => ({
name: service.imageName, name: service.imageName,
appId: service.appId, appId: service.appId,
serviceId: service.serviceId, serviceId: service.serviceId,
@ -47,7 +45,7 @@ const imageForService = (service) => ({
dependent: 0, dependent: 0,
}); });
const fetchAction = (service) => ({ const fetchAction = service => ({
action: 'fetch', action: 'fetch',
image: imageForService(service), image: imageForService(service),
serviceId: service.serviceId, serviceId: service.serviceId,
@ -78,12 +76,7 @@ export class ApplicationManager extends EventEmitter {
this.fetchAction = fetchAction; this.fetchAction = fetchAction;
this._strategySteps = { this._strategySteps = {
'download-then-kill'( 'download-then-kill'(current, target, needsDownload, dependenciesMetForKill) {
current,
target,
needsDownload,
dependenciesMetForKill,
) {
if (needsDownload) { if (needsDownload) {
return fetchAction(target); return fetchAction(target);
} else if (dependenciesMetForKill()) { } else if (dependenciesMetForKill()) {
@ -135,12 +128,8 @@ export class ApplicationManager extends EventEmitter {
this._nextStepsForNetwork = this._nextStepsForNetwork.bind(this); this._nextStepsForNetwork = this._nextStepsForNetwork.bind(this);
this._nextStepForService = this._nextStepForService.bind(this); this._nextStepForService = this._nextStepForService.bind(this);
this._nextStepsForAppUpdate = this._nextStepsForAppUpdate.bind(this); this._nextStepsForAppUpdate = this._nextStepsForAppUpdate.bind(this);
this.setTargetVolatileForService = this.setTargetVolatileForService.bind( this.setTargetVolatileForService = this.setTargetVolatileForService.bind(this);
this, this.clearTargetVolatileForServices = this.clearTargetVolatileForServices.bind(this);
);
this.clearTargetVolatileForServices = this.clearTargetVolatileForServices.bind(
this,
);
this.getTargetApps = this.getTargetApps.bind(this); this.getTargetApps = this.getTargetApps.bind(this);
this.getDependentTargets = this.getDependentTargets.bind(this); this.getDependentTargets = this.getDependentTargets.bind(this);
this._compareImages = this._compareImages.bind(this); this._compareImages = this._compareImages.bind(this);
@ -148,9 +137,7 @@ export class ApplicationManager extends EventEmitter {
this.stopAll = this.stopAll.bind(this); this.stopAll = this.stopAll.bind(this);
this._lockingIfNecessary = this._lockingIfNecessary.bind(this); this._lockingIfNecessary = this._lockingIfNecessary.bind(this);
this.executeStepAction = this.executeStepAction.bind(this); this.executeStepAction = this.executeStepAction.bind(this);
this.getExtraStateForComparison = this.getExtraStateForComparison.bind( this.getExtraStateForComparison = this.getExtraStateForComparison.bind(this);
this,
);
this.getRequiredSteps = this.getRequiredSteps.bind(this); this.getRequiredSteps = this.getRequiredSteps.bind(this);
this.serviceNameFromId = this.serviceNameFromId.bind(this); this.serviceNameFromId = this.serviceNameFromId.bind(this);
this.removeAllVolumesForApp = this.removeAllVolumesForApp.bind(this); this.removeAllVolumesForApp = this.removeAllVolumesForApp.bind(this);
@ -160,7 +147,6 @@ export class ApplicationManager extends EventEmitter {
this.apiBinder = apiBinder; this.apiBinder = apiBinder;
this.services = new ServiceManager(); this.services = new ServiceManager();
this.networks = new NetworkManager();
this.volumes = new VolumeManager(); this.volumes = new VolumeManager();
this.proxyvisor = new Proxyvisor({ this.proxyvisor = new Proxyvisor({
applications: this, applications: this,
@ -174,14 +160,13 @@ export class ApplicationManager extends EventEmitter {
this.actionExecutors = compositionSteps.getExecutors({ this.actionExecutors = compositionSteps.getExecutors({
lockFn: this._lockingIfNecessary, lockFn: this._lockingIfNecessary,
services: this.services, services: this.services,
networks: this.networks,
volumes: this.volumes, volumes: this.volumes,
applications: this, applications: this,
callbacks: { callbacks: {
containerStarted: (id) => { containerStarted: id => {
this._containerStarted[id] = true; this._containerStarted[id] = true;
}, },
containerKilled: (id) => { containerKilled: id => {
delete this._containerStarted[id]; delete this._containerStarted[id];
}, },
fetchStart: () => { fetchStart: () => {
@ -190,16 +175,14 @@ export class ApplicationManager extends EventEmitter {
fetchEnd: () => { fetchEnd: () => {
this.fetchesInProgress -= 1; this.fetchesInProgress -= 1;
}, },
fetchTime: (time) => { fetchTime: time => {
this.timeSpentFetching += time; this.timeSpentFetching += time;
}, },
stateReport: (state) => this.reportCurrentState(state), stateReport: state => this.reportCurrentState(state),
bestDeltaSource: this.bestDeltaSource, bestDeltaSource: this.bestDeltaSource,
}, },
}); });
this.validActions = _.keys(this.actionExecutors).concat( this.validActions = _.keys(this.actionExecutors).concat(this.proxyvisor.validActions);
this.proxyvisor.validActions,
);
this.router = createApplicationManagerRouter(this); this.router = createApplicationManagerRouter(this);
Images.on('change', this.reportCurrentState); Images.on('change', this.reportCurrentState);
this.services.on('change', this.reportCurrentState); this.services.on('change', this.reportCurrentState);
@ -213,7 +196,7 @@ export class ApplicationManager extends EventEmitter {
await Images.initialized; await Images.initialized;
await Images.cleanupDatabase(); await Images.cleanupDatabase();
const cleanup = () => { const cleanup = () => {
return docker.listContainers({ all: true }).then((containers) => { return docker.listContainers({ all: true }).then(containers => {
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id')); return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
}); });
}; };
@ -265,10 +248,7 @@ export class ApplicationManager extends EventEmitter {
); );
} }
if (apps[appId].services[imageId] == null) { if (apps[appId].services[imageId] == null) {
apps[appId].services[imageId] = _.pick(service, [ apps[appId].services[imageId] = _.pick(service, ['status', 'releaseId']);
'status',
'releaseId',
]);
creationTimesAndReleases[appId][imageId] = _.pick(service, [ creationTimesAndReleases[appId][imageId] = _.pick(service, [
'createdAt', 'createdAt',
'releaseId', 'releaseId',
@ -357,7 +337,7 @@ export class ApplicationManager extends EventEmitter {
// multi-app warning! // multi-app warning!
// This is just wrong on every level // This is just wrong on every level
_.each(apps, (app) => { _.each(apps, app => {
app.commit = currentCommit; app.commit = currentCommit;
}); });
@ -367,7 +347,7 @@ export class ApplicationManager extends EventEmitter {
getCurrentForComparison() { getCurrentForComparison() {
return Promise.join( return Promise.join(
this.services.getAll(), this.services.getAll(),
this.networks.getAll(), networkManager.getAll(),
this.volumes.getAll(), this.volumes.getAll(),
config.get('currentCommit'), config.get('currentCommit'),
this._buildApps, this._buildApps,
@ -377,7 +357,7 @@ export class ApplicationManager extends EventEmitter {
getCurrentApp(appId) { getCurrentApp(appId) {
return Promise.join( return Promise.join(
this.services.getAllByAppId(appId), this.services.getAllByAppId(appId),
this.networks.getAllByAppId(appId), networkManager.getAllByAppId(appId),
this.volumes.getAllByAppId(appId), this.volumes.getAllByAppId(appId),
config.get('currentCommit'), config.get('currentCommit'),
this._buildApps, this._buildApps,
@ -422,19 +402,13 @@ export class ApplicationManager extends EventEmitter {
} }
} }
const toBeMaybeUpdated = _.intersection( const toBeMaybeUpdated = _.intersection(targetServiceIds, currentServiceIds);
targetServiceIds,
currentServiceIds,
);
const currentServicesPerId = {}; const currentServicesPerId = {};
const targetServicesPerId = _.keyBy(targetServices, 'serviceId'); const targetServicesPerId = _.keyBy(targetServices, 'serviceId');
for (const serviceId of toBeMaybeUpdated) { for (const serviceId of toBeMaybeUpdated) {
const currentServiceContainers = _.filter(currentServices, { serviceId }); const currentServiceContainers = _.filter(currentServices, { serviceId });
if (currentServiceContainers.length > 1) { if (currentServiceContainers.length > 1) {
currentServicesPerId[serviceId] = _.maxBy( currentServicesPerId[serviceId] = _.maxBy(currentServiceContainers, 'createdAt');
currentServiceContainers,
'createdAt',
);
// All but the latest container for this service are spurious and should be removed // All but the latest container for this service are spurious and should be removed
for (const service of _.without( for (const service of _.without(
@ -454,7 +428,7 @@ export class ApplicationManager extends EventEmitter {
// Returns true if a service matches its target except it should be running and it is not, but we've // Returns true if a service matches its target except it should be running and it is not, but we've
// already started it before. In this case it means it just exited so we don't want to start it again. // already started it before. In this case it means it just exited so we don't want to start it again.
const alreadyStarted = (serviceId) => { const alreadyStarted = serviceId => {
return ( return (
currentServicesPerId[serviceId].isEqualExceptForRunningState( currentServicesPerId[serviceId].isEqualExceptForRunningState(
targetServicesPerId[serviceId], targetServicesPerId[serviceId],
@ -467,7 +441,7 @@ export class ApplicationManager extends EventEmitter {
const needUpdate = _.filter( const needUpdate = _.filter(
toBeMaybeUpdated, toBeMaybeUpdated,
(serviceId) => serviceId =>
!currentServicesPerId[serviceId].isEqual( !currentServicesPerId[serviceId].isEqual(
targetServicesPerId[serviceId], targetServicesPerId[serviceId],
containerIds, containerIds,
@ -502,7 +476,7 @@ export class ApplicationManager extends EventEmitter {
const toBeUpdated = _.filter( const toBeUpdated = _.filter(
_.intersection(targetNames, currentNames), _.intersection(targetNames, currentNames),
(name) => !current[name].isEqualConfig(target[name]), name => !current[name].isEqualConfig(target[name]),
); );
for (const name of toBeUpdated) { for (const name of toBeUpdated) {
outputPairs.push({ outputPairs.push({
@ -515,7 +489,7 @@ export class ApplicationManager extends EventEmitter {
} }
compareNetworksForUpdate({ current, target }) { compareNetworksForUpdate({ current, target }) {
return this._compareNetworksOrVolumesForUpdate(this.networks, { return this._compareNetworksOrVolumesForUpdate(networkManager, {
current, current,
target, target,
}); });
@ -535,8 +509,7 @@ export class ApplicationManager extends EventEmitter {
} }
const hasNetwork = _.some( const hasNetwork = _.some(
networkPairs, networkPairs,
(pair) => pair => `${service.appId}_${pair.current?.name}` === service.networkMode,
`${service.appId}_${pair.current?.name}` === service.networkMode,
); );
if (hasNetwork) { if (hasNetwork) {
return true; return true;
@ -545,7 +518,7 @@ export class ApplicationManager extends EventEmitter {
const name = _.split(volume, ':')[0]; const name = _.split(volume, ':')[0];
return _.some( return _.some(
volumePairs, volumePairs,
(pair) => `${service.appId}_${pair.current?.name}` === name, pair => `${service.appId}_${pair.current?.name}` === name,
); );
}); });
return hasVolume; return hasVolume;
@ -553,15 +526,10 @@ export class ApplicationManager extends EventEmitter {
// TODO: account for volumes-from, networks-from, links, etc // TODO: account for volumes-from, networks-from, links, etc
// TODO: support networks instead of only networkMode // TODO: support networks instead of only networkMode
_dependenciesMetForServiceStart( _dependenciesMetForServiceStart(target, networkPairs, volumePairs, pendingPairs) {
target,
networkPairs,
volumePairs,
pendingPairs,
) {
// for dependsOn, check no install or update pairs have that service // for dependsOn, check no install or update pairs have that service
const dependencyUnmet = _.some(target.dependsOn, (dependency) => const dependencyUnmet = _.some(target.dependsOn, dependency =>
_.some(pendingPairs, (pair) => pair.target?.serviceName === dependency), _.some(pendingPairs, pair => pair.target?.serviceName === dependency),
); );
if (dependencyUnmet) { if (dependencyUnmet) {
return false; return false;
@ -570,7 +538,7 @@ export class ApplicationManager extends EventEmitter {
if ( if (
_.some( _.some(
networkPairs, networkPairs,
(pair) => `${target.appId}_${pair.target?.name}` === target.networkMode, pair => `${target.appId}_${pair.target?.name}` === target.networkMode,
) )
) { ) {
return false; return false;
@ -583,7 +551,7 @@ export class ApplicationManager extends EventEmitter {
} }
return _.some( return _.some(
volumePairs, volumePairs,
(pair) => `${target.appId}_${pair.target?.name}` === sourceName, pair => `${target.appId}_${pair.target?.name}` === sourceName,
); );
}); });
return !volumeUnmet; return !volumeUnmet;
@ -592,12 +560,7 @@ export class ApplicationManager extends EventEmitter {
// Unless the update strategy requires an early kill (i.e. kill-then-download, delete-then-download), we only want // Unless the update strategy requires an early kill (i.e. kill-then-download, delete-then-download), we only want
// to kill a service once the images for the services it depends on have been downloaded, so as to minimize // to kill a service once the images for the services it depends on have been downloaded, so as to minimize
// downtime (but not block the killing too much, potentially causing a deadlock) // downtime (but not block the killing too much, potentially causing a deadlock)
_dependenciesMetForServiceKill( _dependenciesMetForServiceKill(target, targetApp, availableImages, localMode) {
target,
targetApp,
availableImages,
localMode,
) {
// Because we only check for an image being available, in local mode this will always // Because we only check for an image being available, in local mode this will always
// be the case, so return true regardless. If this function ever checks for anything else, // be the case, so return true regardless. If this function ever checks for anything else,
// we'll need to change the logic here // we'll need to change the logic here
@ -612,7 +575,7 @@ export class ApplicationManager extends EventEmitter {
if ( if (
!_.some( !_.some(
availableImages, availableImages,
(image) => image =>
image.dockerImageId === dependencyService.image || image.dockerImageId === dependencyService.image ||
Images.isSameImage(image, { name: dependencyService.imageName }), Images.isSameImage(image, { name: dependencyService.imageName }),
) )
@ -633,7 +596,7 @@ export class ApplicationManager extends EventEmitter {
) { ) {
// Check none of the currentApp.services use this network or volume // Check none of the currentApp.services use this network or volume
if (current != null) { if (current != null) {
const dependencies = _.filter(currentApp.services, (service) => const dependencies = _.filter(currentApp.services, service =>
dependencyComparisonFn(service, current), dependencyComparisonFn(service, current),
); );
if (_.isEmpty(dependencies)) { if (_.isEmpty(dependencies)) {
@ -679,9 +642,7 @@ export class ApplicationManager extends EventEmitter {
const dependencyComparisonFn = (service, curr) => const dependencyComparisonFn = (service, curr) =>
_.some(service.config.volumes, function (volumeDefinition) { _.some(service.config.volumes, function (volumeDefinition) {
const [sourceName, destName] = volumeDefinition.split(':'); const [sourceName, destName] = volumeDefinition.split(':');
return ( return destName != null && sourceName === `${service.appId}_${curr?.name}`;
destName != null && sourceName === `${service.appId}_${curr?.name}`
);
}); });
return this._nextStepsForNetworkOrVolume( return this._nextStepsForNetworkOrVolume(
{ current, target }, { current, target },
@ -694,10 +655,7 @@ export class ApplicationManager extends EventEmitter {
// Infers steps that do not require creating a new container // Infers steps that do not require creating a new container
_updateContainerStep(current, target) { _updateContainerStep(current, target) {
if ( if (current.releaseId !== target.releaseId || current.imageId !== target.imageId) {
current.releaseId !== target.releaseId ||
current.imageId !== target.imageId
) {
return serviceAction('updateMetadata', target.serviceId, current, target); return serviceAction('updateMetadata', target.serviceId, current, target);
} else if (target.config.running) { } else if (target.config.running) {
return serviceAction('start', target.serviceId, current, target); return serviceAction('start', target.serviceId, current, target);
@ -716,12 +674,7 @@ export class ApplicationManager extends EventEmitter {
} }
} }
_nextStepForService( _nextStepForService({ current, target }, updateContext, localMode, containerIds) {
{ current, target },
updateContext,
localMode,
containerIds,
) {
const { const {
targetApp, targetApp,
networkPairs, networkPairs,
@ -746,7 +699,7 @@ export class ApplicationManager extends EventEmitter {
if (!localMode) { if (!localMode) {
needsDownload = !_.some( needsDownload = !_.some(
availableImages, availableImages,
(image) => image =>
image.dockerImageId === target?.config.image || image.dockerImageId === target?.config.image ||
Images.isSameImage(image, { name: target.imageName }), Images.isSameImage(image, { name: target.imageName }),
); );
@ -768,12 +721,7 @@ export class ApplicationManager extends EventEmitter {
const dependenciesMetForKill = () => { const dependenciesMetForKill = () => {
return ( return (
!needsDownload && !needsDownload &&
this._dependenciesMetForServiceKill( this._dependenciesMetForServiceKill(target, targetApp, availableImages, localMode)
target,
targetApp,
availableImages,
localMode,
)
); );
}; };
@ -797,9 +745,7 @@ export class ApplicationManager extends EventEmitter {
dependenciesMetForStart, dependenciesMetForStart,
); );
} else { } else {
let strategy = checkString( let strategy = checkString(target.config.labels['io.balena.update.strategy']);
target.config.labels['io.balena.update.strategy'],
);
const validStrategies = [ const validStrategies = [
'download-then-kill', 'download-then-kill',
'kill-then-download', 'kill-then-download',
@ -809,9 +755,7 @@ export class ApplicationManager extends EventEmitter {
if (!_.includes(validStrategies, strategy)) { if (!_.includes(validStrategies, strategy)) {
strategy = 'download-then-kill'; strategy = 'download-then-kill';
} }
const timeout = checkInt( const timeout = checkInt(target.config.labels['io.balena.update.handover-timeout']);
target.config.labels['io.balena.update.handover-timeout'],
);
return this._strategySteps[strategy]( return this._strategySteps[strategy](
current, current,
target, target,
@ -857,11 +801,8 @@ export class ApplicationManager extends EventEmitter {
if ( if (
currentApp.services?.length === 1 && currentApp.services?.length === 1 &&
targetApp.services?.length === 1 && targetApp.services?.length === 1 &&
targetApp.services[0].serviceName === targetApp.services[0].serviceName === currentApp.services[0].serviceName &&
currentApp.services[0].serviceName && checkTruthy(currentApp.services[0].config.labels['io.balena.legacy-container'])
checkTruthy(
currentApp.services[0].config.labels['io.balena.legacy-container'],
)
) { ) {
// This is a legacy preloaded app or container, so we didn't have things like serviceId. // This is a legacy preloaded app or container, so we didn't have things like serviceId.
// We hack a few things to avoid an unnecessary restart of the preloaded app // We hack a few things to avoid an unnecessary restart of the preloaded app
@ -881,11 +822,7 @@ export class ApplicationManager extends EventEmitter {
current: currentApp.volumes, current: currentApp.volumes,
target: targetApp.volumes, target: targetApp.volumes,
}); });
const { const { removePairs, installPairs, updatePairs } = this.compareServicesForUpdate(
removePairs,
installPairs,
updatePairs,
} = this.compareServicesForUpdate(
currentApp.services, currentApp.services,
targetApp.services, targetApp.services,
containerIds, containerIds,
@ -952,7 +889,7 @@ export class ApplicationManager extends EventEmitter {
} }
const appId = targetApp.appId ?? currentApp.appId; const appId = targetApp.appId ?? currentApp.appId;
return _.map(steps, (step) => _.assign({}, step, { appId })); return _.map(steps, step => _.assign({}, step, { appId }));
} }
async setTarget(apps, dependent, source, maybeTrx) { async setTarget(apps, dependent, source, maybeTrx) {
@ -990,10 +927,7 @@ export class ApplicationManager extends EventEmitter {
const filteredApps = _.cloneDeep(apps); const filteredApps = _.cloneDeep(apps);
_.each( _.each(
fulfilledContracts, fulfilledContracts,
( ({ valid, unmetServices, fulfilledServices, unmetAndOptional }, appId) => {
{ valid, unmetServices, fulfilledServices, unmetAndOptional },
appId,
) => {
if (!valid) { if (!valid) {
contractViolators[apps[appId].name] = unmetServices; contractViolators[apps[appId].name] = unmetServices;
return delete filteredApps[appId]; return delete filteredApps[appId];
@ -1035,17 +969,15 @@ export class ApplicationManager extends EventEmitter {
} }
clearTargetVolatileForServices(imageIds) { clearTargetVolatileForServices(imageIds) {
return imageIds.map( return imageIds.map(imageId => (this._targetVolatilePerImageId[imageId] = {}));
(imageId) => (this._targetVolatilePerImageId[imageId] = {}),
);
} }
async getTargetApps() { async getTargetApps() {
const apps = await dbFormat.getApps(); const apps = await dbFormat.getApps();
_.each(apps, (app) => { _.each(apps, app => {
if (!_.isEmpty(app.services)) { if (!_.isEmpty(app.services)) {
app.services = _.mapValues(app.services, (svc) => { app.services = _.mapValues(app.services, svc => {
if (this._targetVolatilePerImageId[svc.imageId] != null) { if (this._targetVolatilePerImageId[svc.imageId] != null) {
return { return {
...svc, ...svc,
@ -1092,8 +1024,8 @@ export class ApplicationManager extends EventEmitter {
// - are locally available (i.e. an image with the same digest exists) // - are locally available (i.e. an image with the same digest exists)
// - are not saved to the DB with all their metadata (serviceId, serviceName, etc) // - are not saved to the DB with all their metadata (serviceId, serviceName, etc)
_compareImages(current, target, available, localMode) { _compareImages(current, target, available, localMode) {
const allImagesForTargetApp = (app) => _.map(app.services, imageForService); const allImagesForTargetApp = app => _.map(app.services, imageForService);
const allImagesForCurrentApp = (app) => const allImagesForCurrentApp = app =>
_.map(app.services, function (service) { _.map(app.services, function (service) {
const img = const img =
_.find(available, { _.find(available, {
@ -1102,13 +1034,13 @@ export class ApplicationManager extends EventEmitter {
}) ?? _.find(available, { dockerImageId: service.config.image }); }) ?? _.find(available, { dockerImageId: service.config.image });
return _.omit(img, ['dockerImageId', 'id']); return _.omit(img, ['dockerImageId', 'id']);
}); });
const allImageDockerIdsForTargetApp = (app) => const allImageDockerIdsForTargetApp = app =>
_(app.services) _(app.services)
.map((svc) => [svc.imageName, svc.config.image]) .map(svc => [svc.imageName, svc.config.image])
.filter((img) => img[1] != null) .filter(img => img[1] != null)
.value(); .value();
const availableWithoutIds = _.map(available, (image) => const availableWithoutIds = _.map(available, image =>
_.omit(image, ['dockerImageId', 'id']), _.omit(image, ['dockerImageId', 'id']),
); );
const currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp); const currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp);
@ -1119,16 +1051,16 @@ export class ApplicationManager extends EventEmitter {
const availableAndUnused = _.filter( const availableAndUnused = _.filter(
availableWithoutIds, availableWithoutIds,
(image) => image =>
!_.some(currentImages.concat(targetImages), (imageInUse) => !_.some(currentImages.concat(targetImages), imageInUse =>
_.isEqual(image, imageInUse), _.isEqual(image, imageInUse),
), ),
); );
const imagesToDownload = _.filter( const imagesToDownload = _.filter(
targetImages, targetImages,
(targetImage) => targetImage =>
!_.some(available, (availableImage) => !_.some(available, availableImage =>
Images.isSameImage(availableImage, targetImage), Images.isSameImage(availableImage, targetImage),
), ),
); );
@ -1136,46 +1068,39 @@ export class ApplicationManager extends EventEmitter {
let imagesToSave = []; let imagesToSave = [];
if (!localMode) { if (!localMode) {
imagesToSave = _.filter(targetImages, function (targetImage) { imagesToSave = _.filter(targetImages, function (targetImage) {
const isActuallyAvailable = _.some(available, function ( const isActuallyAvailable = _.some(available, function (availableImage) {
availableImage,
) {
if (Images.isSameImage(availableImage, targetImage)) { if (Images.isSameImage(availableImage, targetImage)) {
return true; return true;
} }
if ( if (availableImage.dockerImageId === targetImageDockerIds[targetImage.name]) {
availableImage.dockerImageId ===
targetImageDockerIds[targetImage.name]
) {
return true; return true;
} }
return false; return false;
}); });
const isNotSaved = !_.some(availableWithoutIds, (img) => const isNotSaved = !_.some(availableWithoutIds, img =>
_.isEqual(img, targetImage), _.isEqual(img, targetImage),
); );
return isActuallyAvailable && isNotSaved; return isActuallyAvailable && isNotSaved;
}); });
} }
const deltaSources = _.map(imagesToDownload, (image) => { const deltaSources = _.map(imagesToDownload, image => {
return this.bestDeltaSource(image, available); return this.bestDeltaSource(image, available);
}); });
const proxyvisorImages = this.proxyvisor.imagesInUse(current, target); const proxyvisorImages = this.proxyvisor.imagesInUse(current, target);
const potentialDeleteThenDownload = _.filter( const potentialDeleteThenDownload = _.filter(
current.local.apps.services, current.local.apps.services,
(svc) => svc =>
svc.config.labels['io.balena.update.strategy'] === svc.config.labels['io.balena.update.strategy'] === 'delete-then-download' &&
'delete-then-download' && svc.status === 'Stopped', svc.status === 'Stopped',
); );
const imagesToRemove = _.filter( const imagesToRemove = _.filter(
availableAndUnused.concat(potentialDeleteThenDownload), availableAndUnused.concat(potentialDeleteThenDownload),
function (image) { function (image) {
const notUsedForDelta = !_.includes(deltaSources, image.name); const notUsedForDelta = !_.includes(deltaSources, image.name);
const notUsedByProxyvisor = !_.some( const notUsedByProxyvisor = !_.some(proxyvisorImages, proxyvisorImage =>
proxyvisorImages,
(proxyvisorImage) =>
Images.isSameImage(image, { name: proxyvisorImage }), Images.isSameImage(image, { name: proxyvisorImage }),
); );
return notUsedForDelta && notUsedByProxyvisor; return notUsedForDelta && notUsedByProxyvisor;
@ -1218,10 +1143,8 @@ export class ApplicationManager extends EventEmitter {
// multi-app warning: this will break // multi-app warning: this will break
let appsForVolumeRemoval; let appsForVolumeRemoval;
if (!localMode) { if (!localMode) {
const currentAppIds = _.keys(current.local.apps).map((n) => const currentAppIds = _.keys(current.local.apps).map(n => checkInt(n));
checkInt(n), const targetAppIds = _.keys(target.local.apps).map(n => checkInt(n));
);
const targetAppIds = _.keys(target.local.apps).map((n) => checkInt(n));
appsForVolumeRemoval = _.difference(currentAppIds, targetAppIds); appsForVolumeRemoval = _.difference(currentAppIds, targetAppIds);
} }
@ -1234,15 +1157,11 @@ export class ApplicationManager extends EventEmitter {
const { services } = currentByAppId[appId]; const { services } = currentByAppId[appId];
for (const n in services) { for (const n in services) {
if ( if (
checkTruthy( checkTruthy(services[n].config.labels['io.balena.features.supervisor-api'])
services[n].config.labels['io.balena.features.supervisor-api'],
)
) { ) {
containersUsingSupervisorNetwork = true; containersUsingSupervisorNetwork = true;
if (services[n].status !== 'Stopping') { if (services[n].status !== 'Stopping') {
nextSteps.push( nextSteps.push(serviceAction('kill', services[n].serviceId, services[n]));
serviceAction('kill', services[n].serviceId, services[n]),
);
} else { } else {
nextSteps.push({ action: 'noop' }); nextSteps.push({ action: 'noop' });
} }
@ -1274,10 +1193,7 @@ export class ApplicationManager extends EventEmitter {
} }
// If we have to remove any images, we do that before anything else // If we have to remove any images, we do that before anything else
if (_.isEmpty(nextSteps)) { if (_.isEmpty(nextSteps)) {
const allAppIds = _.union( const allAppIds = _.union(_.keys(currentByAppId), _.keys(targetByAppId));
_.keys(currentByAppId),
_.keys(targetByAppId),
);
for (const appId of allAppIds) { for (const appId of allAppIds) {
nextSteps = nextSteps.concat( nextSteps = nextSteps.concat(
this._nextStepsForAppUpdate( this._nextStepsForAppUpdate(
@ -1294,15 +1210,13 @@ export class ApplicationManager extends EventEmitter {
// the old app to be removed. If it has, we then // the old app to be removed. If it has, we then
// remove all of the volumes // remove all of the volumes
if (_.every(nextSteps, { action: 'noop' })) { if (_.every(nextSteps, { action: 'noop' })) {
volumePromises.push( volumePromises.push(this.removeAllVolumesForApp(checkInt(appId)));
this.removeAllVolumesForApp(checkInt(appId)),
);
} }
} }
} }
} }
} }
const newDownloads = nextSteps.filter((s) => s.action === 'fetch').length; const newDownloads = nextSteps.filter(s => s.action === 'fetch').length;
if (!ignoreImages && delta && newDownloads > 0) { if (!ignoreImages && delta && newDownloads > 0) {
// Check that this is not the first pull for an // Check that this is not the first pull for an
@ -1334,7 +1248,7 @@ export class ApplicationManager extends EventEmitter {
nextSteps.push({ action: 'noop' }); nextSteps.push({ action: 'noop' });
} }
return _.uniqWith(nextSteps, _.isEqual); return _.uniqWith(nextSteps, _.isEqual);
}).then((nextSteps) => }).then(nextSteps =>
Promise.all(volumePromises).then(function (volSteps) { Promise.all(volumePromises).then(function (volSteps) {
nextSteps = nextSteps.concat(_.flatten(volSteps)); nextSteps = nextSteps.concat(_.flatten(volSteps));
return nextSteps; return nextSteps;
@ -1344,18 +1258,14 @@ export class ApplicationManager extends EventEmitter {
stopAll({ force = false, skipLock = false } = {}) { stopAll({ force = false, skipLock = false } = {}) {
return Promise.resolve(this.services.getAll()) return Promise.resolve(this.services.getAll())
.map((service) => { .map(service => {
return this._lockingIfNecessary( return this._lockingIfNecessary(service.appId, { force, skipLock }, () => {
service.appId,
{ force, skipLock },
() => {
return this.services return this.services
.kill(service, { removeContainer: false, wait: true }) .kill(service, { removeContainer: false, wait: true })
.then(() => { .then(() => {
delete this._containerStarted[service.containerId]; delete this._containerStarted[service.containerId];
}); });
}, });
);
}) })
.return(); .return();
} }
@ -1366,10 +1276,8 @@ export class ApplicationManager extends EventEmitter {
} }
return config return config
.get('lockOverride') .get('lockOverride')
.then((lockOverride) => lockOverride || force) .then(lockOverride => lockOverride || force)
.then((lockOverridden) => .then(lockOverridden => updateLock.lock(appId, { force: lockOverridden }, fn));
updateLock.lock(appId, { force: lockOverridden }, fn),
);
} }
executeStepAction(step, { force = false, skipLock = false } = {}) { executeStepAction(step, { force = false, skipLock = false } = {}) {
@ -1379,9 +1287,7 @@ export class ApplicationManager extends EventEmitter {
if (!_.includes(this.validActions, step.action)) { if (!_.includes(this.validActions, step.action)) {
return Promise.reject(new Error(`Invalid action ${step.action}`)); return Promise.reject(new Error(`Invalid action ${step.action}`));
} }
return this.actionExecutors[step.action]( return this.actionExecutors[step.action](_.merge({}, step, { force, skipLock }));
_.merge({}, step, { force, skipLock }),
);
} }
getExtraStateForComparison(currentState, targetState) { getExtraStateForComparison(currentState, targetState) {
@ -1390,7 +1296,7 @@ export class ApplicationManager extends EventEmitter {
.keys() .keys()
.concat(_.keys(targetState.local.apps)) .concat(_.keys(targetState.local.apps))
.uniq() .uniq()
.each((id) => { .each(id => {
const intId = checkInt(id); const intId = checkInt(id);
if (intId == null) { if (intId == null) {
throw new Error(`Invalid id: ${id}`); throw new Error(`Invalid id: ${id}`);
@ -1398,12 +1304,12 @@ export class ApplicationManager extends EventEmitter {
containerIdsByAppId[intId] = this.services.getContainerIdMap(intId); containerIdsByAppId[intId] = this.services.getContainerIdMap(intId);
}); });
return config.get('localMode').then((localMode) => { return config.get('localMode').then(localMode => {
return Promise.props({ return Promise.props({
cleanupNeeded: Images.isCleanupNeeded(), cleanupNeeded: Images.isCleanupNeeded(),
availableImages: Images.getAvailable(), availableImages: Images.getAvailable(),
downloading: Images.getDownloadingImageIds(), downloading: Images.getDownloadingImageIds(),
supervisorNetworkReady: this.networks.supervisorNetworkReady(), supervisorNetworkReady: networkManager.supervisorNetworkReady(),
delta: config.get('delta'), delta: config.get('delta'),
containerIds: Promise.props(containerIdsByAppId), containerIds: Promise.props(containerIdsByAppId),
localMode, localMode,
@ -1439,7 +1345,7 @@ export class ApplicationManager extends EventEmitter {
ignoreImages, ignoreImages,
conf, conf,
containerIds, containerIds,
).then((nextSteps) => { ).then(nextSteps => {
if (ignoreImages && _.some(nextSteps, { action: 'fetch' })) { if (ignoreImages && _.some(nextSteps, { action: 'fetch' })) {
throw new Error('Cannot fetch images while executing an API action'); throw new Error('Cannot fetch images while executing an API action');
} }
@ -1451,7 +1357,7 @@ export class ApplicationManager extends EventEmitter {
targetState, targetState,
nextSteps, nextSteps,
) )
.then((proxyvisorSteps) => nextSteps.concat(proxyvisorSteps)); .then(proxyvisorSteps => nextSteps.concat(proxyvisorSteps));
}); });
} }
@ -1462,10 +1368,7 @@ export class ApplicationManager extends EventEmitter {
// application // application
for (const appId of Object.keys(apps)) { for (const appId of Object.keys(apps)) {
const app = apps[appId]; const app = apps[appId];
const service = _.find( const service = _.find(app.services, svc => svc.serviceId === serviceId);
app.services,
(svc) => svc.serviceId === serviceId,
);
if (service?.serviceName == null) { if (service?.serviceName == null) {
throw new InternalInconsistencyError( throw new InternalInconsistencyError(
`Could not find service name for id: ${serviceId}`, `Could not find service name for id: ${serviceId}`,
@ -1480,8 +1383,8 @@ export class ApplicationManager extends EventEmitter {
} }
removeAllVolumesForApp(appId) { removeAllVolumesForApp(appId) {
return this.volumes.getAllByAppId(appId).then((volumes) => return this.volumes.getAllByAppId(appId).then(volumes =>
volumes.map((v) => ({ volumes.map(v => ({
action: 'removeVolume', action: 'removeVolume',
current: v, current: v,
})), })),
@ -1500,11 +1403,6 @@ export class ApplicationManager extends EventEmitter {
'. ', '. ',
)}`; )}`;
log.info(message); log.info(message);
return logger.logSystemMessage( return logger.logSystemMessage(message, {}, 'optionalContainerViolation', true);
message,
{},
'optionalContainerViolation',
true,
);
} }
} }

View File

@ -11,7 +11,7 @@ import ServiceManager from './service-manager';
import Volume from './volume'; import Volume from './volume';
import { checkTruthy } from '../lib/validation'; import { checkTruthy } from '../lib/validation';
import { NetworkManager } from './network-manager'; import * as networkManager from './network-manager';
import VolumeManager from './volume-manager'; import VolumeManager from './volume-manager';
interface BaseCompositionStepArgs { interface BaseCompositionStepArgs {
@ -137,7 +137,6 @@ interface CompositionCallbacks {
export function getExecutors(app: { export function getExecutors(app: {
lockFn: LockingFn; lockFn: LockingFn;
services: ServiceManager; services: ServiceManager;
networks: NetworkManager;
volumes: VolumeManager; volumes: VolumeManager;
applications: ApplicationManager; applications: ApplicationManager;
callbacks: CompositionCallbacks; callbacks: CompositionCallbacks;
@ -281,19 +280,19 @@ export function getExecutors(app: {
} }
}, },
createNetwork: async (step) => { createNetwork: async (step) => {
await app.networks.create(step.target); await networkManager.create(step.target);
}, },
createVolume: async (step) => { createVolume: async (step) => {
await app.volumes.create(step.target); await app.volumes.create(step.target);
}, },
removeNetwork: async (step) => { removeNetwork: async (step) => {
await app.networks.remove(step.current); await networkManager.remove(step.current);
}, },
removeVolume: async (step) => { removeVolume: async (step) => {
await app.volumes.remove(step.current); await app.volumes.remove(step.current);
}, },
ensureSupervisorNetwork: async () => { ensureSupervisorNetwork: async () => {
app.networks.ensureSupervisorNetwork(); networkManager.ensureSupervisorNetwork();
}, },
}; };

View File

@ -12,9 +12,8 @@ import { Network } from './network';
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
import { ResourceRecreationAttemptError } from './errors'; import { ResourceRecreationAttemptError } from './errors';
export class NetworkManager { export function getAll(): Bluebird<Network[]> {
public getAll(): Bluebird<Network[]> { return getWithBothLabels().map((network: { Name: string }) => {
return this.getWithBothLabels().map((network: { Name: string }) => {
return docker return docker
.getNetwork(network.Name) .getNetwork(network.Name)
.inspect() .inspect()
@ -22,22 +21,25 @@ export class NetworkManager {
return Network.fromDockerNetwork(net); return Network.fromDockerNetwork(net);
}); });
}); });
} }
public getAllByAppId(appId: number): Bluebird<Network[]> { export function getAllByAppId(appId: number): Bluebird<Network[]> {
return this.getAll().filter((network: Network) => network.appId === appId); return getAll().filter((network: Network) => network.appId === appId);
} }
public async get(network: { name: string; appId: number }): Promise<Network> { export async function get(network: {
name: string;
appId: number;
}): Promise<Network> {
const dockerNet = await docker const dockerNet = await docker
.getNetwork(Network.generateDockerName(network.appId, network.name)) .getNetwork(Network.generateDockerName(network.appId, network.name))
.inspect(); .inspect();
return Network.fromDockerNetwork(dockerNet); return Network.fromDockerNetwork(dockerNet);
} }
public async create(network: Network) { export async function create(network: Network) {
try { try {
const existing = await this.get({ const existing = await get({
name: network.name, name: network.name,
appId: network.appId, appId: network.appId,
}); });
@ -59,22 +61,20 @@ export class NetworkManager {
// If we got a not found error, create the network // If we got a not found error, create the network
await network.create(); await network.create();
} }
} }
public async remove(network: Network) { export async function remove(network: Network) {
// We simply forward this to the network object, but we // We simply forward this to the network object, but we
// add this method to provide a consistent interface // add this method to provide a consistent interface
await network.remove(); await network.remove();
} }
public supervisorNetworkReady(): Bluebird<boolean> { export function supervisorNetworkReady(): Bluebird<boolean> {
return Bluebird.resolve( return Bluebird.resolve(
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`), fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
) )
.then(() => { .then(() => {
return docker return docker.getNetwork(constants.supervisorNetworkInterface).inspect();
.getNetwork(constants.supervisorNetworkInterface)
.inspect();
}) })
.then((network) => { .then((network) => {
return ( return (
@ -86,16 +86,14 @@ export class NetworkManager {
}) })
.catchReturn(NotFoundError, false) .catchReturn(NotFoundError, false)
.catchReturn(ENOENT, false); .catchReturn(ENOENT, false);
} }
public ensureSupervisorNetwork(): Bluebird<void> { export function ensureSupervisorNetwork(): Bluebird<void> {
const removeIt = () => { const removeIt = () => {
return Bluebird.resolve( return Bluebird.resolve(
docker.getNetwork(constants.supervisorNetworkInterface).remove(), docker.getNetwork(constants.supervisorNetworkInterface).remove(),
).then(() => { ).then(() => {
return docker return docker.getNetwork(constants.supervisorNetworkInterface).inspect();
.getNetwork(constants.supervisorNetworkInterface)
.inspect();
}); });
}; };
@ -139,9 +137,9 @@ export class NetworkManager {
}), }),
); );
}); });
} }
private getWithBothLabels() { function getWithBothLabels() {
return Bluebird.join( return Bluebird.join(
docker.listNetworks({ docker.listNetworks({
filters: { filters: {
@ -157,5 +155,4 @@ export class NetworkManager {
return _.unionBy(currentNetworks, legacyNetworks, 'Id'); return _.unionBy(currentNetworks, legacyNetworks, 'Id');
}, },
); );
}
} }

View File

@ -3,7 +3,7 @@ import { fs } from 'mz';
import { stub } from 'sinon'; import { stub } from 'sinon';
import { ApplicationManager } from '../../src/application-manager'; import { ApplicationManager } from '../../src/application-manager';
import { NetworkManager } from '../../src/compose/network-manager'; import * as networkManager from '../../src/compose/network-manager';
import { ServiceManager } from '../../src/compose/service-manager'; import { ServiceManager } from '../../src/compose/service-manager';
import { VolumeManager } from '../../src/compose/volume-manager'; import { VolumeManager } from '../../src/compose/volume-manager';
import * as config from '../../src/config'; import * as config from '../../src/config';
@ -133,20 +133,23 @@ function buildRoutes(appManager: ApplicationManager): Router {
return router; return router;
} }
const originalNetGetAll = networkManager.getAllByAppId;
function setupStubs() { function setupStubs() {
stub(ServiceManager.prototype, 'getStatus').resolves(STUBBED_VALUES.services); stub(ServiceManager.prototype, 'getStatus').resolves(STUBBED_VALUES.services);
stub(NetworkManager.prototype, 'getAllByAppId').resolves(
STUBBED_VALUES.networks,
);
stub(VolumeManager.prototype, 'getAllByAppId').resolves( stub(VolumeManager.prototype, 'getAllByAppId').resolves(
STUBBED_VALUES.volumes, STUBBED_VALUES.volumes,
); );
// @ts-expect-error Assigning to a RO property
networkManager.getAllByAppId = () => Promise.resolve(STUBBED_VALUES.networks);
} }
function restoreStubs() { function restoreStubs() {
(ServiceManager.prototype as any).getStatus.restore(); (ServiceManager.prototype as any).getStatus.restore();
(NetworkManager.prototype as any).getAllByAppId.restore();
(VolumeManager.prototype as any).getAllByAppId.restore(); (VolumeManager.prototype as any).getAllByAppId.restore();
// @ts-expect-error Assigning to a RO property
networkManager.getAllByAppId = originalNetGetAll;
} }
interface SupervisorAPIOpts { interface SupervisorAPIOpts {