mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-22 03:55:22 +00:00
Merge pull request #1373 from balena-io/singleton-managers
Make network-manager, volume-manager and service-manager singletons
This commit is contained in:
commit
54cbfface2
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ test/data/led_file
|
||||
report.xml
|
||||
.DS_Store
|
||||
.tsbuildinfo
|
||||
.prettierrc
|
||||
|
8
src/application-manager.d.ts
vendored
8
src/application-manager.d.ts
vendored
@ -7,15 +7,11 @@ import { ServiceAction } from './device-api/common';
|
||||
import { DeviceStatus, InstancedAppState } from './types/state';
|
||||
|
||||
import type { Image } from './compose/images';
|
||||
import ServiceManager from './compose/service-manager';
|
||||
import DeviceState from './device-state';
|
||||
|
||||
import { APIBinder } from './api-binder';
|
||||
import * as config from './config';
|
||||
|
||||
import NetworkManager from './compose/network-manager';
|
||||
import VolumeManager from './compose/volume-manager';
|
||||
|
||||
import {
|
||||
CompositionStep,
|
||||
CompositionStepAction,
|
||||
@ -48,10 +44,6 @@ class ApplicationManager extends EventEmitter {
|
||||
public deviceState: DeviceState;
|
||||
public apiBinder: APIBinder;
|
||||
|
||||
public services: ServiceManager;
|
||||
public volumes: VolumeManager;
|
||||
public networks: NetworkManager;
|
||||
|
||||
public proxyvisor: any;
|
||||
public timeSpentFetching: number;
|
||||
public fetchesInProgress: number;
|
||||
|
@ -21,11 +21,11 @@ import {
|
||||
|
||||
import * as dbFormat from './device-state/db-format';
|
||||
|
||||
import { Network } from './compose/network';
|
||||
import { ServiceManager } from './compose/service-manager';
|
||||
import * as Images from './compose/images';
|
||||
import { NetworkManager } from './compose/network-manager';
|
||||
import { VolumeManager } from './compose/volume-manager';
|
||||
import { Network } from './compose/network';
|
||||
import * as networkManager from './compose/network-manager';
|
||||
import * as volumeManager from './compose/volume-manager';
|
||||
import * as serviceManager from './compose/service-manager';
|
||||
import * as compositionSteps from './compose/composition-steps';
|
||||
|
||||
import { Proxyvisor } from './proxyvisor';
|
||||
@ -159,9 +159,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
this.deviceState = deviceState;
|
||||
this.apiBinder = apiBinder;
|
||||
|
||||
this.services = new ServiceManager();
|
||||
this.networks = new NetworkManager();
|
||||
this.volumes = new VolumeManager();
|
||||
this.proxyvisor = new Proxyvisor({
|
||||
applications: this,
|
||||
});
|
||||
@ -173,9 +170,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
|
||||
this.actionExecutors = compositionSteps.getExecutors({
|
||||
lockFn: this._lockingIfNecessary,
|
||||
services: this.services,
|
||||
networks: this.networks,
|
||||
volumes: this.volumes,
|
||||
applications: this,
|
||||
callbacks: {
|
||||
containerStarted: (id) => {
|
||||
@ -202,7 +196,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
);
|
||||
this.router = createApplicationManagerRouter(this);
|
||||
Images.on('change', this.reportCurrentState);
|
||||
this.services.on('change', this.reportCurrentState);
|
||||
serviceManager.on('change', this.reportCurrentState);
|
||||
}
|
||||
|
||||
reportCurrentState(data) {
|
||||
@ -227,14 +221,14 @@ export class ApplicationManager extends EventEmitter {
|
||||
// But also run it in on startup
|
||||
await cleanup();
|
||||
await this.localModeManager.init();
|
||||
await this.services.attachToRunning();
|
||||
await this.services.listenToEvents();
|
||||
await serviceManager.attachToRunning();
|
||||
await serviceManager.listenToEvents();
|
||||
}
|
||||
|
||||
// Returns the status of applications and their services
|
||||
getStatus() {
|
||||
return Promise.join(
|
||||
this.services.getStatus(),
|
||||
serviceManager.getStatus(),
|
||||
Images.getStatus(),
|
||||
config.get('currentCommit'),
|
||||
function (services, images, currentCommit) {
|
||||
@ -366,9 +360,9 @@ export class ApplicationManager extends EventEmitter {
|
||||
|
||||
getCurrentForComparison() {
|
||||
return Promise.join(
|
||||
this.services.getAll(),
|
||||
this.networks.getAll(),
|
||||
this.volumes.getAll(),
|
||||
serviceManager.getAll(),
|
||||
networkManager.getAll(),
|
||||
volumeManager.getAll(),
|
||||
config.get('currentCommit'),
|
||||
this._buildApps,
|
||||
);
|
||||
@ -376,9 +370,9 @@ export class ApplicationManager extends EventEmitter {
|
||||
|
||||
getCurrentApp(appId) {
|
||||
return Promise.join(
|
||||
this.services.getAllByAppId(appId),
|
||||
this.networks.getAllByAppId(appId),
|
||||
this.volumes.getAllByAppId(appId),
|
||||
serviceManager.getAllByAppId(appId),
|
||||
networkManager.getAllByAppId(appId),
|
||||
volumeManager.getAllByAppId(appId),
|
||||
config.get('currentCommit'),
|
||||
this._buildApps,
|
||||
).get(appId);
|
||||
@ -515,14 +509,14 @@ export class ApplicationManager extends EventEmitter {
|
||||
}
|
||||
|
||||
compareNetworksForUpdate({ current, target }) {
|
||||
return this._compareNetworksOrVolumesForUpdate(this.networks, {
|
||||
return this._compareNetworksOrVolumesForUpdate(networkManager, {
|
||||
current,
|
||||
target,
|
||||
});
|
||||
}
|
||||
|
||||
compareVolumesForUpdate({ current, target }) {
|
||||
return this._compareNetworksOrVolumesForUpdate(this.volumes, {
|
||||
return this._compareNetworksOrVolumesForUpdate(volumeManager, {
|
||||
current,
|
||||
target,
|
||||
});
|
||||
@ -1343,13 +1337,13 @@ export class ApplicationManager extends EventEmitter {
|
||||
}
|
||||
|
||||
stopAll({ force = false, skipLock = false } = {}) {
|
||||
return Promise.resolve(this.services.getAll())
|
||||
return Promise.resolve(serviceManager.getAll())
|
||||
.map((service) => {
|
||||
return this._lockingIfNecessary(
|
||||
service.appId,
|
||||
{ force, skipLock },
|
||||
() => {
|
||||
return this.services
|
||||
return serviceManager
|
||||
.kill(service, { removeContainer: false, wait: true })
|
||||
.then(() => {
|
||||
delete this._containerStarted[service.containerId];
|
||||
@ -1395,7 +1389,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
if (intId == null) {
|
||||
throw new Error(`Invalid id: ${id}`);
|
||||
}
|
||||
containerIdsByAppId[intId] = this.services.getContainerIdMap(intId);
|
||||
containerIdsByAppId[intId] = serviceManager.getContainerIdMap(intId);
|
||||
});
|
||||
|
||||
return config.get('localMode').then((localMode) => {
|
||||
@ -1403,7 +1397,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
cleanupNeeded: Images.isCleanupNeeded(),
|
||||
availableImages: Images.getAvailable(),
|
||||
downloading: Images.getDownloadingImageIds(),
|
||||
supervisorNetworkReady: this.networks.supervisorNetworkReady(),
|
||||
supervisorNetworkReady: networkManager.supervisorNetworkReady(),
|
||||
delta: config.get('delta'),
|
||||
containerIds: Promise.props(containerIdsByAppId),
|
||||
localMode,
|
||||
@ -1480,7 +1474,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
}
|
||||
|
||||
removeAllVolumesForApp(appId) {
|
||||
return this.volumes.getAllByAppId(appId).then((volumes) =>
|
||||
return volumeManager.getAllByAppId(appId).then((volumes) =>
|
||||
volumes.map((v) => ({
|
||||
action: 'removeVolume',
|
||||
current: v,
|
||||
|
@ -7,12 +7,12 @@ import type { Image } from './images';
|
||||
import * as images from './images';
|
||||
import Network from './network';
|
||||
import Service from './service';
|
||||
import ServiceManager from './service-manager';
|
||||
import * as serviceManager from './service-manager';
|
||||
import Volume from './volume';
|
||||
|
||||
import { checkTruthy } from '../lib/validation';
|
||||
import { NetworkManager } from './network-manager';
|
||||
import VolumeManager from './volume-manager';
|
||||
import * as networkManager from './network-manager';
|
||||
import * as volumeManager from './volume-manager';
|
||||
|
||||
interface BaseCompositionStepArgs {
|
||||
force?: boolean;
|
||||
@ -136,9 +136,6 @@ interface CompositionCallbacks {
|
||||
|
||||
export function getExecutors(app: {
|
||||
lockFn: LockingFn;
|
||||
services: ServiceManager;
|
||||
networks: NetworkManager;
|
||||
volumes: VolumeManager;
|
||||
applications: ApplicationManager;
|
||||
callbacks: CompositionCallbacks;
|
||||
}) {
|
||||
@ -152,7 +149,7 @@ export function getExecutors(app: {
|
||||
},
|
||||
async () => {
|
||||
const wait = _.get(step, ['options', 'wait'], false);
|
||||
await app.services.kill(step.current, {
|
||||
await serviceManager.kill(step.current, {
|
||||
removeContainer: false,
|
||||
wait,
|
||||
});
|
||||
@ -168,7 +165,7 @@ export function getExecutors(app: {
|
||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
||||
},
|
||||
async () => {
|
||||
await app.services.kill(step.current);
|
||||
await serviceManager.kill(step.current);
|
||||
app.callbacks.containerKilled(step.current.containerId);
|
||||
if (_.get(step, ['options', 'removeImage'])) {
|
||||
await images.removeByDockerId(step.current.config.image);
|
||||
@ -179,7 +176,7 @@ export function getExecutors(app: {
|
||||
remove: async (step) => {
|
||||
// Only called for dead containers, so no need to
|
||||
// take locks
|
||||
await app.services.remove(step.current);
|
||||
await serviceManager.remove(step.current);
|
||||
},
|
||||
updateMetadata: (step) => {
|
||||
const skipLock =
|
||||
@ -192,7 +189,7 @@ export function getExecutors(app: {
|
||||
skipLock: skipLock || _.get(step, ['options', 'skipLock']),
|
||||
},
|
||||
async () => {
|
||||
await app.services.updateMetadata(step.current, step.target);
|
||||
await serviceManager.updateMetadata(step.current, step.target);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -204,9 +201,9 @@ export function getExecutors(app: {
|
||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
||||
},
|
||||
async () => {
|
||||
await app.services.kill(step.current, { wait: true });
|
||||
await serviceManager.kill(step.current, { wait: true });
|
||||
app.callbacks.containerKilled(step.current.containerId);
|
||||
const container = await app.services.start(step.target);
|
||||
const container = await serviceManager.start(step.target);
|
||||
app.callbacks.containerStarted(container.id);
|
||||
},
|
||||
);
|
||||
@ -218,7 +215,7 @@ export function getExecutors(app: {
|
||||
});
|
||||
},
|
||||
start: async (step) => {
|
||||
const container = await app.services.start(step.target);
|
||||
const container = await serviceManager.start(step.target);
|
||||
app.callbacks.containerStarted(container.id);
|
||||
},
|
||||
updateCommit: async (step) => {
|
||||
@ -232,7 +229,7 @@ export function getExecutors(app: {
|
||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
||||
},
|
||||
async () => {
|
||||
await app.services.handover(step.current, step.target);
|
||||
await serviceManager.handover(step.current, step.target);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -281,19 +278,19 @@ export function getExecutors(app: {
|
||||
}
|
||||
},
|
||||
createNetwork: async (step) => {
|
||||
await app.networks.create(step.target);
|
||||
await networkManager.create(step.target);
|
||||
},
|
||||
createVolume: async (step) => {
|
||||
await app.volumes.create(step.target);
|
||||
await volumeManager.create(step.target);
|
||||
},
|
||||
removeNetwork: async (step) => {
|
||||
await app.networks.remove(step.current);
|
||||
await networkManager.remove(step.current);
|
||||
},
|
||||
removeVolume: async (step) => {
|
||||
await app.volumes.remove(step.current);
|
||||
await volumeManager.remove(step.current);
|
||||
},
|
||||
ensureSupervisorNetwork: async () => {
|
||||
app.networks.ensureSupervisorNetwork();
|
||||
networkManager.ensureSupervisorNetwork();
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -12,150 +12,147 @@ import { Network } from './network';
|
||||
import log from '../lib/supervisor-console';
|
||||
import { ResourceRecreationAttemptError } from './errors';
|
||||
|
||||
export class NetworkManager {
|
||||
public getAll(): Bluebird<Network[]> {
|
||||
return this.getWithBothLabels().map((network: { Name: string }) => {
|
||||
return docker
|
||||
.getNetwork(network.Name)
|
||||
.inspect()
|
||||
.then((net) => {
|
||||
return Network.fromDockerNetwork(net);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getAllByAppId(appId: number): Bluebird<Network[]> {
|
||||
return this.getAll().filter((network: Network) => network.appId === appId);
|
||||
}
|
||||
|
||||
public async get(network: { name: string; appId: number }): Promise<Network> {
|
||||
const dockerNet = await docker
|
||||
.getNetwork(Network.generateDockerName(network.appId, network.name))
|
||||
.inspect();
|
||||
return Network.fromDockerNetwork(dockerNet);
|
||||
}
|
||||
|
||||
public async create(network: Network) {
|
||||
try {
|
||||
const existing = await this.get({
|
||||
name: network.name,
|
||||
appId: network.appId,
|
||||
});
|
||||
if (!network.isEqualConfig(existing)) {
|
||||
throw new ResourceRecreationAttemptError('network', network.name);
|
||||
}
|
||||
|
||||
// We have a network with the same config and name
|
||||
// already created, we can skip this
|
||||
} catch (e) {
|
||||
if (!NotFoundError(e)) {
|
||||
logger.logSystemEvent(logTypes.createNetworkError, {
|
||||
network: { name: network.name, appId: network.appId },
|
||||
error: e,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If we got a not found error, create the network
|
||||
await network.create();
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(network: Network) {
|
||||
// We simply forward this to the network object, but we
|
||||
// add this method to provide a consistent interface
|
||||
await network.remove();
|
||||
}
|
||||
|
||||
public supervisorNetworkReady(): Bluebird<boolean> {
|
||||
return Bluebird.resolve(
|
||||
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
||||
)
|
||||
.then(() => {
|
||||
return docker
|
||||
.getNetwork(constants.supervisorNetworkInterface)
|
||||
.inspect();
|
||||
})
|
||||
.then((network) => {
|
||||
return (
|
||||
network.Options['com.docker.network.bridge.name'] ===
|
||||
constants.supervisorNetworkInterface &&
|
||||
network.IPAM.Config[0].Subnet === constants.supervisorNetworkSubnet &&
|
||||
network.IPAM.Config[0].Gateway === constants.supervisorNetworkGateway
|
||||
);
|
||||
})
|
||||
.catchReturn(NotFoundError, false)
|
||||
.catchReturn(ENOENT, false);
|
||||
}
|
||||
|
||||
public ensureSupervisorNetwork(): Bluebird<void> {
|
||||
const removeIt = () => {
|
||||
return Bluebird.resolve(
|
||||
docker.getNetwork(constants.supervisorNetworkInterface).remove(),
|
||||
).then(() => {
|
||||
return docker
|
||||
.getNetwork(constants.supervisorNetworkInterface)
|
||||
.inspect();
|
||||
});
|
||||
};
|
||||
|
||||
return Bluebird.resolve(
|
||||
docker.getNetwork(constants.supervisorNetworkInterface).inspect(),
|
||||
)
|
||||
export function getAll(): Bluebird<Network[]> {
|
||||
return getWithBothLabels().map((network: { Name: string }) => {
|
||||
return docker
|
||||
.getNetwork(network.Name)
|
||||
.inspect()
|
||||
.then((net) => {
|
||||
if (
|
||||
net.Options['com.docker.network.bridge.name'] !==
|
||||
constants.supervisorNetworkInterface ||
|
||||
net.IPAM.Config[0].Subnet !== constants.supervisorNetworkSubnet ||
|
||||
net.IPAM.Config[0].Gateway !== constants.supervisorNetworkGateway
|
||||
) {
|
||||
return removeIt();
|
||||
} else {
|
||||
return Bluebird.resolve(
|
||||
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
||||
)
|
||||
.catch(ENOENT, removeIt)
|
||||
.return();
|
||||
}
|
||||
})
|
||||
.catch(NotFoundError, () => {
|
||||
log.debug(`Creating ${constants.supervisorNetworkInterface} network`);
|
||||
return Bluebird.resolve(
|
||||
docker.createNetwork({
|
||||
Name: constants.supervisorNetworkInterface,
|
||||
Options: {
|
||||
'com.docker.network.bridge.name':
|
||||
constants.supervisorNetworkInterface,
|
||||
},
|
||||
IPAM: {
|
||||
Driver: 'default',
|
||||
Config: [
|
||||
{
|
||||
Subnet: constants.supervisorNetworkSubnet,
|
||||
Gateway: constants.supervisorNetworkGateway,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return Network.fromDockerNetwork(net);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getWithBothLabels() {
|
||||
return Bluebird.join(
|
||||
docker.listNetworks({
|
||||
filters: {
|
||||
label: ['io.resin.supervised'],
|
||||
},
|
||||
}),
|
||||
docker.listNetworks({
|
||||
filters: {
|
||||
label: ['io.balena.supervised'],
|
||||
},
|
||||
}),
|
||||
(legacyNetworks, currentNetworks) => {
|
||||
return _.unionBy(currentNetworks, legacyNetworks, 'Id');
|
||||
},
|
||||
);
|
||||
export function getAllByAppId(appId: number): Bluebird<Network[]> {
|
||||
return getAll().filter((network: Network) => network.appId === appId);
|
||||
}
|
||||
|
||||
export async function get(network: {
|
||||
name: string;
|
||||
appId: number;
|
||||
}): Promise<Network> {
|
||||
const dockerNet = await docker
|
||||
.getNetwork(Network.generateDockerName(network.appId, network.name))
|
||||
.inspect();
|
||||
return Network.fromDockerNetwork(dockerNet);
|
||||
}
|
||||
|
||||
export async function create(network: Network) {
|
||||
try {
|
||||
const existing = await get({
|
||||
name: network.name,
|
||||
appId: network.appId,
|
||||
});
|
||||
if (!network.isEqualConfig(existing)) {
|
||||
throw new ResourceRecreationAttemptError('network', network.name);
|
||||
}
|
||||
|
||||
// We have a network with the same config and name
|
||||
// already created, we can skip this
|
||||
} catch (e) {
|
||||
if (!NotFoundError(e)) {
|
||||
logger.logSystemEvent(logTypes.createNetworkError, {
|
||||
network: { name: network.name, appId: network.appId },
|
||||
error: e,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If we got a not found error, create the network
|
||||
await network.create();
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(network: Network) {
|
||||
// We simply forward this to the network object, but we
|
||||
// add this method to provide a consistent interface
|
||||
await network.remove();
|
||||
}
|
||||
|
||||
export function supervisorNetworkReady(): Bluebird<boolean> {
|
||||
return Bluebird.resolve(
|
||||
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
||||
)
|
||||
.then(() => {
|
||||
return docker.getNetwork(constants.supervisorNetworkInterface).inspect();
|
||||
})
|
||||
.then((network) => {
|
||||
return (
|
||||
network.Options['com.docker.network.bridge.name'] ===
|
||||
constants.supervisorNetworkInterface &&
|
||||
network.IPAM.Config[0].Subnet === constants.supervisorNetworkSubnet &&
|
||||
network.IPAM.Config[0].Gateway === constants.supervisorNetworkGateway
|
||||
);
|
||||
})
|
||||
.catchReturn(NotFoundError, false)
|
||||
.catchReturn(ENOENT, false);
|
||||
}
|
||||
|
||||
export function ensureSupervisorNetwork(): Bluebird<void> {
|
||||
const removeIt = () => {
|
||||
return Bluebird.resolve(
|
||||
docker.getNetwork(constants.supervisorNetworkInterface).remove(),
|
||||
).then(() => {
|
||||
return docker.getNetwork(constants.supervisorNetworkInterface).inspect();
|
||||
});
|
||||
};
|
||||
|
||||
return Bluebird.resolve(
|
||||
docker.getNetwork(constants.supervisorNetworkInterface).inspect(),
|
||||
)
|
||||
.then((net) => {
|
||||
if (
|
||||
net.Options['com.docker.network.bridge.name'] !==
|
||||
constants.supervisorNetworkInterface ||
|
||||
net.IPAM.Config[0].Subnet !== constants.supervisorNetworkSubnet ||
|
||||
net.IPAM.Config[0].Gateway !== constants.supervisorNetworkGateway
|
||||
) {
|
||||
return removeIt();
|
||||
} else {
|
||||
return Bluebird.resolve(
|
||||
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
|
||||
)
|
||||
.catch(ENOENT, removeIt)
|
||||
.return();
|
||||
}
|
||||
})
|
||||
.catch(NotFoundError, () => {
|
||||
log.debug(`Creating ${constants.supervisorNetworkInterface} network`);
|
||||
return Bluebird.resolve(
|
||||
docker.createNetwork({
|
||||
Name: constants.supervisorNetworkInterface,
|
||||
Options: {
|
||||
'com.docker.network.bridge.name':
|
||||
constants.supervisorNetworkInterface,
|
||||
},
|
||||
IPAM: {
|
||||
Driver: 'default',
|
||||
Config: [
|
||||
{
|
||||
Subnet: constants.supervisorNetworkSubnet,
|
||||
Gateway: constants.supervisorNetworkGateway,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getWithBothLabels() {
|
||||
return Bluebird.join(
|
||||
docker.listNetworks({
|
||||
filters: {
|
||||
label: ['io.resin.supervised'],
|
||||
},
|
||||
}),
|
||||
docker.listNetworks({
|
||||
filters: {
|
||||
label: ['io.balena.supervised'],
|
||||
},
|
||||
}),
|
||||
(legacyNetworks, currentNetworks) => {
|
||||
return _.unionBy(currentNetworks, legacyNetworks, 'Id');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -17,143 +17,139 @@ export interface VolumeNameOpts {
|
||||
appId: number;
|
||||
}
|
||||
|
||||
export class VolumeManager {
|
||||
public async get({ name, appId }: VolumeNameOpts): Promise<Volume> {
|
||||
return Volume.fromDockerVolume(
|
||||
await docker.getVolume(Volume.generateDockerName(appId, name)).inspect(),
|
||||
);
|
||||
}
|
||||
export async function get({ name, appId }: VolumeNameOpts): Promise<Volume> {
|
||||
return Volume.fromDockerVolume(
|
||||
await docker.getVolume(Volume.generateDockerName(appId, name)).inspect(),
|
||||
);
|
||||
}
|
||||
|
||||
public async getAll(): Promise<Volume[]> {
|
||||
const volumeInspect = await this.listWithBothLabels();
|
||||
return volumeInspect.map((inspect) => Volume.fromDockerVolume(inspect));
|
||||
}
|
||||
export async function getAll(): Promise<Volume[]> {
|
||||
const volumeInspect = await listWithBothLabels();
|
||||
return volumeInspect.map((inspect) => Volume.fromDockerVolume(inspect));
|
||||
}
|
||||
|
||||
public async getAllByAppId(appId: number): Promise<Volume[]> {
|
||||
const all = await this.getAll();
|
||||
return _.filter(all, { appId });
|
||||
}
|
||||
export async function getAllByAppId(appId: number): Promise<Volume[]> {
|
||||
const all = await getAll();
|
||||
return _.filter(all, { appId });
|
||||
}
|
||||
|
||||
public async create(volume: Volume): Promise<void> {
|
||||
// First we check that we're not trying to recreate a
|
||||
// volume
|
||||
try {
|
||||
const existing = await this.get({
|
||||
name: volume.name,
|
||||
appId: volume.appId,
|
||||
export async function create(volume: Volume): Promise<void> {
|
||||
// First we check that we're not trying to recreate a
|
||||
// volume
|
||||
try {
|
||||
const existing = await get({
|
||||
name: volume.name,
|
||||
appId: volume.appId,
|
||||
});
|
||||
|
||||
if (!volume.isEqualConfig(existing)) {
|
||||
throw new ResourceRecreationAttemptError('volume', volume.name);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!NotFoundError(e)) {
|
||||
logger.logSystemEvent(LogTypes.createVolumeError, {
|
||||
volume: { name: volume.name },
|
||||
error: e,
|
||||
});
|
||||
|
||||
if (!volume.isEqualConfig(existing)) {
|
||||
throw new ResourceRecreationAttemptError('volume', volume.name);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!NotFoundError(e)) {
|
||||
logger.logSystemEvent(LogTypes.createVolumeError, {
|
||||
volume: { name: volume.name },
|
||||
error: e,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
|
||||
await volume.create();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// We simply forward this to the volume object, but we
|
||||
// add this method to provide a consistent interface
|
||||
public async remove(volume: Volume) {
|
||||
await volume.remove();
|
||||
}
|
||||
|
||||
public async createFromLegacy(appId: number): Promise<Volume | void> {
|
||||
const name = defaultLegacyVolume();
|
||||
const legacyPath = Path.join(
|
||||
constants.rootMountPoint,
|
||||
'mnt/data/resin-data',
|
||||
appId.toString(),
|
||||
);
|
||||
|
||||
try {
|
||||
return await this.createFromPath({ name, appId }, {}, legacyPath);
|
||||
} catch (e) {
|
||||
logger.logSystemMessage(
|
||||
`Warning: could not migrate legacy /data volume: ${e.message}`,
|
||||
{ error: e },
|
||||
'Volume migration error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async createFromPath(
|
||||
{ name, appId }: VolumeNameOpts,
|
||||
config: Partial<VolumeConfig>,
|
||||
oldPath: string,
|
||||
): Promise<Volume> {
|
||||
const volume = Volume.fromComposeObject(name, appId, config);
|
||||
|
||||
await this.create(volume);
|
||||
const inspect = await docker
|
||||
.getVolume(Volume.generateDockerName(volume.appId, volume.name))
|
||||
.inspect();
|
||||
|
||||
const volumePath = Path.join(
|
||||
constants.rootMountPoint,
|
||||
'mnt/data',
|
||||
...inspect.Mountpoint.split(Path.sep).slice(3),
|
||||
);
|
||||
|
||||
await safeRename(oldPath, volumePath);
|
||||
return volume;
|
||||
}
|
||||
|
||||
public async removeOrphanedVolumes(
|
||||
referencedVolumes: string[],
|
||||
): Promise<void> {
|
||||
// Iterate through every container, and track the
|
||||
// references to a volume
|
||||
// Note that we're not just interested in containers
|
||||
// which are part of the private state, and instead
|
||||
// *all* containers. This means we don't remove
|
||||
// something that's part of a sideloaded container
|
||||
const [dockerContainers, dockerVolumes] = await Promise.all([
|
||||
docker.listContainers(),
|
||||
docker.listVolumes(),
|
||||
]);
|
||||
|
||||
const containerVolumes = _(dockerContainers)
|
||||
.flatMap((c) => c.Mounts)
|
||||
.filter((m) => m.Type === 'volume')
|
||||
// We know that the name must be set, if the mount is
|
||||
// a volume
|
||||
.map((m) => m.Name as string)
|
||||
.uniq()
|
||||
.value();
|
||||
const volumeNames = _.map(dockerVolumes.Volumes, 'Name');
|
||||
|
||||
const volumesToRemove = _.difference(
|
||||
volumeNames,
|
||||
containerVolumes,
|
||||
// Don't remove any volume which is still referenced
|
||||
// in the target state
|
||||
referencedVolumes,
|
||||
);
|
||||
await Promise.all(volumesToRemove.map((v) => docker.getVolume(v).remove()));
|
||||
}
|
||||
|
||||
private async listWithBothLabels(): Promise<VolumeInspectInfo[]> {
|
||||
const [legacyResponse, currentResponse] = await Promise.all([
|
||||
docker.listVolumes({
|
||||
filters: { label: ['io.resin.supervised'] },
|
||||
}),
|
||||
docker.listVolumes({
|
||||
filters: { label: ['io.balena.supervised'] },
|
||||
}),
|
||||
]);
|
||||
|
||||
const legacyVolumes = _.get(legacyResponse, 'Volumes', []);
|
||||
const currentVolumes = _.get(currentResponse, 'Volumes', []);
|
||||
return _.unionBy(legacyVolumes, currentVolumes, 'Name');
|
||||
await volume.create();
|
||||
}
|
||||
}
|
||||
|
||||
export default VolumeManager;
|
||||
// We simply forward this to the volume object, but we
|
||||
// add this method to provide a consistent interface
|
||||
export async function remove(volume: Volume) {
|
||||
await volume.remove();
|
||||
}
|
||||
|
||||
export async function createFromLegacy(appId: number): Promise<Volume | void> {
|
||||
const name = defaultLegacyVolume();
|
||||
const legacyPath = Path.join(
|
||||
constants.rootMountPoint,
|
||||
'mnt/data/resin-data',
|
||||
appId.toString(),
|
||||
);
|
||||
|
||||
try {
|
||||
return await createFromPath({ name, appId }, {}, legacyPath);
|
||||
} catch (e) {
|
||||
logger.logSystemMessage(
|
||||
`Warning: could not migrate legacy /data volume: ${e.message}`,
|
||||
{ error: e },
|
||||
'Volume migration error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFromPath(
|
||||
{ name, appId }: VolumeNameOpts,
|
||||
config: Partial<VolumeConfig>,
|
||||
oldPath: string,
|
||||
): Promise<Volume> {
|
||||
const volume = Volume.fromComposeObject(name, appId, config);
|
||||
|
||||
await create(volume);
|
||||
const inspect = await docker
|
||||
.getVolume(Volume.generateDockerName(volume.appId, volume.name))
|
||||
.inspect();
|
||||
|
||||
const volumePath = Path.join(
|
||||
constants.rootMountPoint,
|
||||
'mnt/data',
|
||||
...inspect.Mountpoint.split(Path.sep).slice(3),
|
||||
);
|
||||
|
||||
await safeRename(oldPath, volumePath);
|
||||
return volume;
|
||||
}
|
||||
|
||||
export async function removeOrphanedVolumes(
|
||||
referencedVolumes: string[],
|
||||
): Promise<void> {
|
||||
// Iterate through every container, and track the
|
||||
// references to a volume
|
||||
// Note that we're not just interested in containers
|
||||
// which are part of the private state, and instead
|
||||
// *all* containers. This means we don't remove
|
||||
// something that's part of a sideloaded container
|
||||
const [dockerContainers, dockerVolumes] = await Promise.all([
|
||||
docker.listContainers(),
|
||||
docker.listVolumes(),
|
||||
]);
|
||||
|
||||
const containerVolumes = _(dockerContainers)
|
||||
.flatMap((c) => c.Mounts)
|
||||
.filter((m) => m.Type === 'volume')
|
||||
// We know that the name must be set, if the mount is
|
||||
// a volume
|
||||
.map((m) => m.Name as string)
|
||||
.uniq()
|
||||
.value();
|
||||
const volumeNames = _.map(dockerVolumes.Volumes, 'Name');
|
||||
|
||||
const volumesToRemove = _.difference(
|
||||
volumeNames,
|
||||
containerVolumes,
|
||||
// Don't remove any volume which is still referenced
|
||||
// in the target state
|
||||
referencedVolumes,
|
||||
);
|
||||
await Promise.all(volumesToRemove.map((v) => docker.getVolume(v).remove()));
|
||||
}
|
||||
|
||||
async function listWithBothLabels(): Promise<VolumeInspectInfo[]> {
|
||||
const [legacyResponse, currentResponse] = await Promise.all([
|
||||
docker.listVolumes({
|
||||
filters: { label: ['io.resin.supervised'] },
|
||||
}),
|
||||
docker.listVolumes({
|
||||
filters: { label: ['io.balena.supervised'] },
|
||||
}),
|
||||
]);
|
||||
|
||||
const legacyVolumes = _.get(legacyResponse, 'Volumes', []);
|
||||
const currentVolumes = _.get(currentResponse, 'Volumes', []);
|
||||
return _.unionBy(legacyVolumes, currentVolumes, 'Name');
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
import * as logger from '../logger';
|
||||
import * as images from '../compose/images';
|
||||
import * as volumeManager from '../compose/volume-manager';
|
||||
import * as serviceManager from '../compose/service-manager';
|
||||
import { spawnJournalctl } from '../lib/journald';
|
||||
import {
|
||||
appNotFoundMessage,
|
||||
@ -152,7 +154,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
// It's kinda hacky to access the services and db via the application manager
|
||||
// maybe refactor this code
|
||||
Bluebird.join(
|
||||
applications.services.getStatus(),
|
||||
serviceManager.getStatus(),
|
||||
images.getStatus(),
|
||||
db.models('app').select(['appId', 'commit', 'name']),
|
||||
(
|
||||
@ -358,7 +360,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
});
|
||||
|
||||
router.get('/v2/containerId', async (req, res) => {
|
||||
const services = await applications.services.getAll();
|
||||
const services = await serviceManager.getAll();
|
||||
|
||||
if (req.query.serviceName != null || req.query.service != null) {
|
||||
const serviceName = req.query.serviceName || req.query.service;
|
||||
@ -392,7 +394,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
const currentRelease = await config.get('currentCommit');
|
||||
|
||||
const pending = applications.deviceState.applyInProgress;
|
||||
const containerStates = (await applications.services.getAll()).map((svc) =>
|
||||
const containerStates = (await serviceManager.getAll()).map((svc) =>
|
||||
_.pick(
|
||||
svc,
|
||||
'status',
|
||||
@ -484,7 +486,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
referencedVolumes.push(Volume.generateDockerName(vol.appId, vol.name));
|
||||
});
|
||||
});
|
||||
await applications.volumes.removeOrphanedVolumes(referencedVolumes);
|
||||
await volumeManager.removeOrphanedVolumes(referencedVolumes);
|
||||
res.json({
|
||||
status: 'success',
|
||||
});
|
||||
|
@ -12,6 +12,8 @@ const rimrafAsync = Bluebird.promisify(rimraf);
|
||||
import { ApplicationManager } from '../application-manager';
|
||||
import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
import * as volumeManager from '../compose/volume-manager';
|
||||
import * as serviceManager from '../compose/service-manager';
|
||||
import DeviceState from '../device-state';
|
||||
import * as constants from '../lib/constants';
|
||||
import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors';
|
||||
@ -243,13 +245,13 @@ export async function normaliseLegacyDatabase(
|
||||
}
|
||||
|
||||
log.debug('Killing legacy containers');
|
||||
await application.services.killAllLegacy();
|
||||
await serviceManager.killAllLegacy();
|
||||
log.debug('Migrating legacy app volumes');
|
||||
|
||||
const targetApps = await application.getTargetApps();
|
||||
|
||||
for (const appId of _.keys(targetApps)) {
|
||||
await application.volumes.createFromLegacy(parseInt(appId, 10));
|
||||
await volumeManager.createFromLegacy(parseInt(appId, 10));
|
||||
}
|
||||
|
||||
await config.set({
|
||||
@ -302,7 +304,7 @@ export async function loadBackupFromMigration(
|
||||
if (volumes[volumeName] != null) {
|
||||
log.debug(`Creating volume ${volumeName} from backup`);
|
||||
// If the volume exists (from a previous incomplete run of this restoreBackup), we delete it first
|
||||
await deviceState.applications.volumes
|
||||
await volumeManager
|
||||
.get({ appId, name: volumeName })
|
||||
.then((volume) => {
|
||||
return volume.remove();
|
||||
@ -314,7 +316,7 @@ export async function loadBackupFromMigration(
|
||||
throw error;
|
||||
});
|
||||
|
||||
await deviceState.applications.volumes.createFromPath(
|
||||
await volumeManager.createFromPath(
|
||||
{ appId, name: volumeName },
|
||||
volumes[volumeName],
|
||||
path.join(backupPath, volumeName),
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { fs } from 'mz';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import { ApplicationManager } from '../../src/application-manager';
|
||||
import { NetworkManager } from '../../src/compose/network-manager';
|
||||
import { ServiceManager } from '../../src/compose/service-manager';
|
||||
import { VolumeManager } from '../../src/compose/volume-manager';
|
||||
import * as networkManager from '../../src/compose/network-manager';
|
||||
import * as serviceManager from '../../src/compose/service-manager';
|
||||
import * as volumeManager from '../../src/compose/volume-manager';
|
||||
import * as config from '../../src/config';
|
||||
import * as db from '../../src/db';
|
||||
import { createV1Api } from '../../src/device-api/v1';
|
||||
@ -133,20 +132,25 @@ function buildRoutes(appManager: ApplicationManager): Router {
|
||||
return router;
|
||||
}
|
||||
|
||||
const originalNetGetAll = networkManager.getAllByAppId;
|
||||
const originalVolGetAll = volumeManager.getAllByAppId;
|
||||
const originalSvcGetStatus = serviceManager.getStatus;
|
||||
function setupStubs() {
|
||||
stub(ServiceManager.prototype, 'getStatus').resolves(STUBBED_VALUES.services);
|
||||
stub(NetworkManager.prototype, 'getAllByAppId').resolves(
|
||||
STUBBED_VALUES.networks,
|
||||
);
|
||||
stub(VolumeManager.prototype, 'getAllByAppId').resolves(
|
||||
STUBBED_VALUES.volumes,
|
||||
);
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
networkManager.getAllByAppId = async () => STUBBED_VALUES.networks;
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
volumeManager.getAllByAppId = async () => STUBBED_VALUES.volumes;
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
serviceManager.getStatus = async () => STUBBED_VALUES.services;
|
||||
}
|
||||
|
||||
function restoreStubs() {
|
||||
(ServiceManager.prototype as any).getStatus.restore();
|
||||
(NetworkManager.prototype as any).getAllByAppId.restore();
|
||||
(VolumeManager.prototype as any).getAllByAppId.restore();
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
networkManager.getAllByAppId = originalNetGetAll;
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
volumeManager.getAllByAppId = originalVolGetAll;
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
serviceManager.getStatus = originalSvcGetStatus;
|
||||
}
|
||||
|
||||
interface SupervisorAPIOpts {
|
||||
|
Loading…
x
Reference in New Issue
Block a user