Merge pull request #1351 from balena-io/singleton-docker

Make docker module a singleton
This commit is contained in:
bulldozer-balena[bot] 2020-06-02 19:08:55 +00:00 committed by GitHub
commit 96e55585ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 430 additions and 489 deletions

View File

@ -25,7 +25,6 @@ import {
import Network from './compose/network'; import Network from './compose/network';
import Service from './compose/service'; import Service from './compose/service';
import Volume from './compose/volume'; import Volume from './compose/volume';
import DockerUtils from './lib/docker-utils';
declare interface Options { declare interface Options {
force?: boolean; force?: boolean;
@ -52,7 +51,6 @@ class ApplicationManager extends EventEmitter {
public deviceState: DeviceState; public deviceState: DeviceState;
public eventTracker: EventTracker; public eventTracker: EventTracker;
public apiBinder: APIBinder; public apiBinder: APIBinder;
public docker: DockerUtils;
public services: ServiceManager; public services: ServiceManager;
public volumes: VolumeManager; public volumes: VolumeManager;

View File

@ -11,7 +11,8 @@ import { log } from './lib/supervisor-console';
import * as config from './config'; import * as config from './config';
import { validateTargetContracts } from './lib/contracts'; import { validateTargetContracts } from './lib/contracts';
import { DockerUtils as Docker } from './lib/docker-utils'; import { docker } from './lib/docker-utils';
import * as dockerUtils 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';
@ -172,30 +173,24 @@ export class ApplicationManager extends EventEmitter {
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.deviceState = deviceState; this.deviceState = deviceState;
this.apiBinder = apiBinder; this.apiBinder = apiBinder;
this.docker = new Docker();
this.images = new Images({ this.images = new Images({
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
this.services = new ServiceManager({ this.services = new ServiceManager({
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
this.networks = new NetworkManager({ this.networks = new NetworkManager({
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
this.volumes = new VolumeManager({ this.volumes = new VolumeManager({
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
this.proxyvisor = new Proxyvisor({ this.proxyvisor = new Proxyvisor({
logger: this.logger, logger: this.logger,
docker: this.docker,
images: this.images, images: this.images,
applications: this, applications: this,
}); });
this.localModeManager = new LocalModeManager(this.docker, this.logger); this.localModeManager = new LocalModeManager(this.logger);
this.timeSpentFetching = 0; this.timeSpentFetching = 0;
this.fetchesInProgress = 0; this.fetchesInProgress = 0;
this._targetVolatilePerImageId = {}; this._targetVolatilePerImageId = {};
@ -257,9 +252,7 @@ export class ApplicationManager extends EventEmitter {
}) })
.then(() => { .then(() => {
const cleanup = () => { const cleanup = () => {
return this.docker return docker.listContainers({ all: true }).then((containers) => {
.listContainers({ all: true })
.then((containers) => {
return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id')); return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
}); });
}; };
@ -1061,14 +1054,12 @@ export class ApplicationManager extends EventEmitter {
createTargetVolume(name, appId, volume) { createTargetVolume(name, appId, volume) {
return Volume.fromComposeObject(name, appId, volume, { return Volume.fromComposeObject(name, appId, volume, {
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
} }
createTargetNetwork(name, appId, network) { createTargetNetwork(name, appId, network) {
return Network.fromComposeObject(name, appId, network, { return Network.fromComposeObject(name, appId, network, {
docker: this.docker,
logger: this.logger, logger: this.logger,
}); });
} }
@ -1076,7 +1067,7 @@ export class ApplicationManager extends EventEmitter {
normaliseAndExtendAppFromDB(app) { normaliseAndExtendAppFromDB(app) {
return Promise.join( return Promise.join(
config.get('extendedEnvOptions'), config.get('extendedEnvOptions'),
this.docker dockerUtils
.getNetworkGateway(constants.supervisorNetworkInterface) .getNetworkGateway(constants.supervisorNetworkInterface)
.catch(() => '127.0.0.1'), .catch(() => '127.0.0.1'),
Promise.props({ Promise.props({

View File

@ -8,9 +8,11 @@ import * as db from '../db';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { import {
DeltaFetchOptions, DeltaFetchOptions,
DockerUtils,
FetchOptions, FetchOptions,
docker,
dockerToolbelt,
} from '../lib/docker-utils'; } from '../lib/docker-utils';
import * as dockerUtils from '../lib/docker-utils';
import { DeltaStillProcessingError, NotFoundError } from '../lib/errors'; import { DeltaStillProcessingError, NotFoundError } from '../lib/errors';
import * as LogTypes from '../lib/log-types'; import * as LogTypes from '../lib/log-types';
import * as validation from '../lib/validation'; import * as validation from '../lib/validation';
@ -26,7 +28,6 @@ interface ImageEvents {
type ImageEventEmitter = StrictEventEmitter<EventEmitter, ImageEvents>; type ImageEventEmitter = StrictEventEmitter<EventEmitter, ImageEvents>;
interface ImageConstructOpts { interface ImageConstructOpts {
docker: DockerUtils;
logger: Logger; logger: Logger;
} }
@ -56,7 +57,6 @@ type NormalisedDockerImage = Docker.ImageInfo & {
}; };
export class Images extends (EventEmitter as new () => ImageEventEmitter) { export class Images extends (EventEmitter as new () => ImageEventEmitter) {
private docker: DockerUtils;
private logger: Logger; private logger: Logger;
public appUpdatePollInterval: number; public appUpdatePollInterval: number;
@ -72,7 +72,6 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
public constructor(opts: ImageConstructOpts) { public constructor(opts: ImageConstructOpts) {
super(); super();
this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
} }
@ -202,7 +201,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
cb: (dockerImages: NormalisedDockerImage[], composeImages: Image[]) => T, cb: (dockerImages: NormalisedDockerImage[], composeImages: Image[]) => T,
) { ) {
const [normalisedImages, dbImages] = await Promise.all([ const [normalisedImages, dbImages] = await Promise.all([
Bluebird.map(this.docker.listImages({ digests: true }), async (image) => { Bluebird.map(docker.listImages({ digests: true }), async (image) => {
const newImage = _.clone(image) as NormalisedDockerImage; const newImage = _.clone(image) as NormalisedDockerImage;
newImage.NormalisedRepoTags = await this.getNormalisedTags(image); newImage.NormalisedRepoTags = await this.getNormalisedTags(image);
return newImage; return newImage;
@ -337,8 +336,8 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
supervisorImage, supervisorImage,
usedImageIds, usedImageIds,
] = await Promise.all([ ] = await Promise.all([
this.docker.getRegistryAndName(constants.supervisorImage), dockerToolbelt.getRegistryAndName(constants.supervisorImage),
this.docker.getImage(constants.supervisorImage).inspect(), docker.getImage(constants.supervisorImage).inspect(),
db db
.models('image') .models('image')
.select('dockerImageId') .select('dockerImageId')
@ -366,7 +365,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
); );
}; };
const dockerImages = await this.docker.listImages({ digests: true }); const dockerImages = await docker.listImages({ digests: true });
for (const image of dockerImages) { for (const image of dockerImages) {
// Cleanup should remove truly dangling images (i.e dangling and with no digests) // Cleanup should remove truly dangling images (i.e dangling and with no digests)
if (Images.isDangling(image) && !_.includes(usedImageIds, image.Id)) { if (Images.isDangling(image) && !_.includes(usedImageIds, image.Id)) {
@ -377,7 +376,9 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
) { ) {
// We also remove images from the supervisor repository with a different tag // We also remove images from the supervisor repository with a different tag
for (const tag of image.RepoTags) { for (const tag of image.RepoTags) {
const imageNameComponents = await this.docker.getRegistryAndName(tag); const imageNameComponents = await dockerToolbelt.getRegistryAndName(
tag,
);
if (isSupervisorRepoTag(imageNameComponents)) { if (isSupervisorRepoTag(imageNameComponents)) {
images.push(image.Id); images.push(image.Id);
} }
@ -400,7 +401,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
imageName: string, imageName: string,
): Promise<Docker.ImageInspectInfo> { ): Promise<Docker.ImageInspectInfo> {
try { try {
return await this.docker.getImage(imageName).inspect(); return await docker.getImage(imageName).inspect();
} catch (e) { } catch (e) {
if (NotFoundError(e)) { if (NotFoundError(e)) {
const digest = imageName.split('@')[1]; const digest = imageName.split('@')[1];
@ -418,7 +419,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
for (const image of imagesFromDb) { for (const image of imagesFromDb) {
if (image.dockerImageId != null) { if (image.dockerImageId != null) {
return await this.docker.getImage(image.dockerImageId).inspect(); return await docker.getImage(image.dockerImageId).inspect();
} }
} }
} }
@ -435,7 +436,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
for (const image of images) { for (const image of images) {
log.debug(`Cleaning up ${image}`); log.debug(`Cleaning up ${image}`);
try { try {
await this.docker.getImage(image).remove({ force: true }); await docker.getImage(image).remove({ force: true });
delete this.imageCleanupFailures[image]; delete this.imageCleanupFailures[image];
} catch (e) { } catch (e) {
this.logger.logSystemMessage( this.logger.logSystemMessage(
@ -459,7 +460,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
} }
public normalise(imageName: string): Bluebird<string> { public normalise(imageName: string): Bluebird<string> {
return this.docker.normaliseImageName(imageName); return dockerToolbelt.normaliseImageName(imageName);
} }
private static isDangling(image: Docker.ImageInfo): boolean { private static isDangling(image: Docker.ImageInfo): boolean {
@ -496,7 +497,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
if (img.dockerImageId == null) { if (img.dockerImageId == null) {
// Legacy image from before we started using dockerImageId, so we try to remove it // Legacy image from before we started using dockerImageId, so we try to remove it
// by name // by name
await this.docker.getImage(img.name).remove({ force: true }); await docker.getImage(img.name).remove({ force: true });
removed = true; removed = true;
} else { } else {
const imagesFromDb = await db const imagesFromDb = await db
@ -512,11 +513,11 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
_.merge(_.clone(image), { status: 'Deleting' }), _.merge(_.clone(image), { status: 'Deleting' }),
); );
this.logger.logSystemEvent(LogTypes.deleteImage, { image }); this.logger.logSystemEvent(LogTypes.deleteImage, { image });
this.docker.getImage(img.dockerImageId).remove({ force: true }); docker.getImage(img.dockerImageId).remove({ force: true });
removed = true; removed = true;
} else if (!Images.hasDigest(img.name)) { } else if (!Images.hasDigest(img.name)) {
// Image has a regular tag, so we might have to remove unnecessary tags // Image has a regular tag, so we might have to remove unnecessary tags
const dockerImage = await this.docker const dockerImage = await docker
.getImage(img.dockerImageId) .getImage(img.dockerImageId)
.inspect(); .inspect();
const differentTags = _.reject(imagesFromDb, { name: img.name }); const differentTags = _.reject(imagesFromDb, { name: img.name });
@ -528,7 +529,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
_.some(differentTags, { name: t }), _.some(differentTags, { name: t }),
) )
) { ) {
await this.docker.getImage(img.name).remove({ noprune: true }); await docker.getImage(img.name).remove({ noprune: true });
} }
removed = false; removed = false;
} else { } else {
@ -589,7 +590,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
const srcImage = await this.inspectByName(deltaOpts.deltaSource); const srcImage = await this.inspectByName(deltaOpts.deltaSource);
deltaOpts.deltaSourceId = srcImage.Id; deltaOpts.deltaSourceId = srcImage.Id;
const id = await this.docker.fetchDeltaWithProgress( const id = await dockerUtils.fetchDeltaWithProgress(
image.name, image.name,
deltaOpts, deltaOpts,
onProgress, onProgress,
@ -597,8 +598,8 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
); );
if (!Images.hasDigest(image.name)) { if (!Images.hasDigest(image.name)) {
const { repo, tag } = await this.docker.getRepoAndTag(image.name); const { repo, tag } = await dockerUtils.getRepoAndTag(image.name);
await this.docker.getImage(id).tag({ repo, tag }); await docker.getImage(id).tag({ repo, tag });
} }
return id; return id;
@ -610,7 +611,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
onProgress: (evt: FetchProgressEvent) => void, onProgress: (evt: FetchProgressEvent) => void,
): Promise<string> { ): Promise<string> {
this.logger.logSystemEvent(LogTypes.downloadImage, { image }); this.logger.logSystemEvent(LogTypes.downloadImage, { image });
return this.docker.fetchImageWithProgress(image.name, opts, onProgress); return dockerUtils.fetchImageWithProgress(image.name, opts, onProgress);
} }
// TODO: find out if imageId can actually be null // TODO: find out if imageId can actually be null

View File

@ -3,7 +3,7 @@ import * as _ from 'lodash';
import { fs } from 'mz'; import { fs } from 'mz';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import Docker from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
import { ENOENT, NotFoundError } from '../lib/errors'; import { ENOENT, NotFoundError } from '../lib/errors';
import logTypes = require('../lib/log-types'); import logTypes = require('../lib/log-types');
import { Logger } from '../logger'; import { Logger } from '../logger';
@ -13,23 +13,20 @@ import log from '../lib/supervisor-console';
import { ResourceRecreationAttemptError } from './errors'; import { ResourceRecreationAttemptError } from './errors';
export class NetworkManager { export class NetworkManager {
private docker: Docker;
private logger: Logger; private logger: Logger;
constructor(opts: NetworkOptions) { constructor(opts: NetworkOptions) {
this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
} }
public getAll(): Bluebird<Network[]> { public getAll(): Bluebird<Network[]> {
return this.getWithBothLabels().map((network: { Name: string }) => { return this.getWithBothLabels().map((network: { Name: string }) => {
return this.docker return docker
.getNetwork(network.Name) .getNetwork(network.Name)
.inspect() .inspect()
.then((net) => { .then((net) => {
return Network.fromDockerNetwork( return Network.fromDockerNetwork(
{ {
docker: this.docker,
logger: this.logger, logger: this.logger,
}, },
net, net,
@ -43,13 +40,10 @@ export class NetworkManager {
} }
public async get(network: { name: string; appId: number }): Promise<Network> { public async get(network: { name: string; appId: number }): Promise<Network> {
const dockerNet = await this.docker const dockerNet = await docker
.getNetwork(Network.generateDockerName(network.appId, network.name)) .getNetwork(Network.generateDockerName(network.appId, network.name))
.inspect(); .inspect();
return Network.fromDockerNetwork( return Network.fromDockerNetwork({ logger: this.logger }, dockerNet);
{ docker: this.docker, logger: this.logger },
dockerNet,
);
} }
public async create(network: Network) { public async create(network: Network) {
@ -89,7 +83,7 @@ export class NetworkManager {
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`), fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
) )
.then(() => { .then(() => {
return this.docker return docker
.getNetwork(constants.supervisorNetworkInterface) .getNetwork(constants.supervisorNetworkInterface)
.inspect(); .inspect();
}) })
@ -108,16 +102,16 @@ export class NetworkManager {
public ensureSupervisorNetwork(): Bluebird<void> { public ensureSupervisorNetwork(): Bluebird<void> {
const removeIt = () => { const removeIt = () => {
return Bluebird.resolve( return Bluebird.resolve(
this.docker.getNetwork(constants.supervisorNetworkInterface).remove(), docker.getNetwork(constants.supervisorNetworkInterface).remove(),
).then(() => { ).then(() => {
return this.docker return docker
.getNetwork(constants.supervisorNetworkInterface) .getNetwork(constants.supervisorNetworkInterface)
.inspect(); .inspect();
}); });
}; };
return Bluebird.resolve( return Bluebird.resolve(
this.docker.getNetwork(constants.supervisorNetworkInterface).inspect(), docker.getNetwork(constants.supervisorNetworkInterface).inspect(),
) )
.then((net) => { .then((net) => {
if ( if (
@ -138,7 +132,7 @@ export class NetworkManager {
.catch(NotFoundError, () => { .catch(NotFoundError, () => {
log.debug(`Creating ${constants.supervisorNetworkInterface} network`); log.debug(`Creating ${constants.supervisorNetworkInterface} network`);
return Bluebird.resolve( return Bluebird.resolve(
this.docker.createNetwork({ docker.createNetwork({
Name: constants.supervisorNetworkInterface, Name: constants.supervisorNetworkInterface,
Options: { Options: {
'com.docker.network.bridge.name': 'com.docker.network.bridge.name':
@ -160,12 +154,12 @@ export class NetworkManager {
private getWithBothLabels() { private getWithBothLabels() {
return Bluebird.join( return Bluebird.join(
this.docker.listNetworks({ docker.listNetworks({
filters: { filters: {
label: ['io.resin.supervised'], label: ['io.resin.supervised'],
}, },
}), }),
this.docker.listNetworks({ docker.listNetworks({
filters: { filters: {
label: ['io.balena.supervised'], label: ['io.balena.supervised'],
}, },

View File

@ -1,7 +1,7 @@
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Docker from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
import { InvalidAppIdError } from '../lib/errors'; import { InvalidAppIdError } from '../lib/errors';
import logTypes = require('../lib/log-types'); import logTypes = require('../lib/log-types');
import { checkInt } from '../lib/validation'; import { checkInt } from '../lib/validation';
@ -22,7 +22,6 @@ import {
} from './errors'; } from './errors';
export interface NetworkOptions { export interface NetworkOptions {
docker: Docker;
logger: Logger; logger: Logger;
} }
@ -31,11 +30,9 @@ export class Network {
public name: string; public name: string;
public config: NetworkConfig; public config: NetworkConfig;
private docker: Docker;
private logger: Logger; private logger: Logger;
private constructor(opts: NetworkOptions) { private constructor(opts: NetworkOptions) {
this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
} }
@ -145,7 +142,7 @@ export class Network {
network: { name: this.name }, network: { name: this.name },
}); });
return await this.docker.createNetwork(this.toDockerConfig()); return await docker.createNetwork(this.toDockerConfig());
} }
public toDockerConfig(): DockerNetworkConfig { public toDockerConfig(): DockerNetworkConfig {
@ -191,7 +188,7 @@ export class Network {
}); });
return Bluebird.resolve( return Bluebird.resolve(
this.docker docker
.getNetwork(Network.generateDockerName(this.appId, this.name)) .getNetwork(Network.generateDockerName(this.appId, this.name))
.remove(), .remove(),
).tapCatch((error) => { ).tapCatch((error) => {

View File

@ -8,7 +8,7 @@ import { fs } from 'mz';
import StrictEventEmitter from 'strict-event-emitter-types'; import StrictEventEmitter from 'strict-event-emitter-types';
import * as config from '../config'; import * as config from '../config';
import Docker from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
import Logger from '../logger'; import Logger from '../logger';
import { PermissiveNumber } from '../config/types'; import { PermissiveNumber } from '../config/types';
@ -26,7 +26,6 @@ import { serviceNetworksToDockerNetworks } from './utils';
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
interface ServiceConstructOpts { interface ServiceConstructOpts {
docker: Docker;
logger: Logger; logger: Logger;
} }
@ -44,7 +43,6 @@ interface KillOpts {
} }
export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) { export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) {
private docker: Docker;
private logger: Logger; private logger: Logger;
// Whether a container has died, indexed by ID // Whether a container has died, indexed by ID
@ -56,7 +54,6 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
public constructor(opts: ServiceConstructOpts) { public constructor(opts: ServiceConstructOpts) {
super(); super();
this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
} }
@ -68,7 +65,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
const services = await Bluebird.map(containers, async (container) => { const services = await Bluebird.map(containers, async (container) => {
try { try {
const serviceInspect = await this.docker const serviceInspect = await docker
.getContainer(container.Id) .getContainer(container.Id)
.inspect(); .inspect();
const service = Service.fromDockerContainer(serviceInspect); const service = Service.fromDockerContainer(serviceInspect);
@ -138,7 +135,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
public async getByDockerContainerId( public async getByDockerContainerId(
containerId: string, containerId: string,
): Promise<Service | null> { ): Promise<Service | null> {
const container = await this.docker.getContainer(containerId).inspect(); const container = await docker.getContainer(containerId).inspect();
if ( if (
container.Config.Labels['io.balena.supervised'] == null && container.Config.Labels['io.balena.supervised'] == null &&
container.Config.Labels['io.resin.supervised'] == null container.Config.Labels['io.resin.supervised'] == null
@ -159,7 +156,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
); );
} }
await this.docker.getContainer(svc.containerId).rename({ await docker.getContainer(svc.containerId).rename({
name: `${service.serviceName}_${metadata.imageId}_${metadata.releaseId}`, name: `${service.serviceName}_${metadata.imageId}_${metadata.releaseId}`,
}); });
} }
@ -180,10 +177,10 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
// Containers haven't been normalized (this is an updated supervisor) // Containers haven't been normalized (this is an updated supervisor)
// so we need to stop and remove them // so we need to stop and remove them
const supervisorImageId = ( const supervisorImageId = (
await this.docker.getImage(constants.supervisorImage).inspect() await docker.getImage(constants.supervisorImage).inspect()
).Id; ).Id;
for (const container of await this.docker.listContainers({ all: true })) { for (const container of await docker.listContainers({ all: true })) {
if (container.ImageID !== supervisorImageId) { if (container.ImageID !== supervisorImageId) {
await this.killContainer(container.Id, { await this.killContainer(container.Id, {
serviceName: 'legacy', serviceName: 'legacy',
@ -212,7 +209,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
} }
try { try {
await this.docker await docker
.getContainer(existingService.containerId) .getContainer(existingService.containerId)
.remove({ v: true }); .remove({ v: true });
} catch (e) { } catch (e) {
@ -244,7 +241,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
`No containerId provided for service ${service.serviceName} in ServiceManager.updateMetadata. Service: ${service}`, `No containerId provided for service ${service.serviceName} in ServiceManager.updateMetadata. Service: ${service}`,
); );
} }
return this.docker.getContainer(existing.containerId); return docker.getContainer(existing.containerId);
} catch (e) { } catch (e) {
if (!NotFoundError(e)) { if (!NotFoundError(e)) {
this.logger.logSystemEvent(LogTypes.installServiceError, { this.logger.logSystemEvent(LogTypes.installServiceError, {
@ -280,12 +277,12 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
this.logger.logSystemEvent(LogTypes.installService, { service }); this.logger.logSystemEvent(LogTypes.installService, { service });
this.reportNewStatus(mockContainerId, service, 'Installing'); this.reportNewStatus(mockContainerId, service, 'Installing');
const container = await this.docker.createContainer(conf); const container = await docker.createContainer(conf);
service.containerId = container.id; service.containerId = container.id;
await Promise.all( await Promise.all(
_.map((nets || {}).EndpointsConfig, (endpointConfig, name) => _.map((nets || {}).EndpointsConfig, (endpointConfig, name) =>
this.docker.getNetwork(name).connect({ docker.getNetwork(name).connect({
Container: container.id, Container: container.id,
EndpointConfig: endpointConfig, EndpointConfig: endpointConfig,
}), }),
@ -366,7 +363,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
); );
} }
this.logger.attach(this.docker, container.id, { serviceId, imageId }); this.logger.attach(container.id, { serviceId, imageId });
if (!alreadyStarted) { if (!alreadyStarted) {
this.logger.logSystemEvent(LogTypes.startServiceSuccess, { service }); this.logger.logSystemEvent(LogTypes.startServiceSuccess, { service });
@ -389,10 +386,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
this.listening = true; this.listening = true;
const listen = async () => { const listen = async () => {
const stream = await this.docker.getEvents({ const stream = await docker.getEvents({
// Remove the as any once
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/43100
// is merged and released
filters: { type: ['container'] } as any, filters: { type: ['container'] } as any,
}); });
@ -434,7 +428,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
`serviceId and imageId not defined for service: ${service.serviceName} in ServiceManager.listenToEvents`, `serviceId and imageId not defined for service: ${service.serviceName} in ServiceManager.listenToEvents`,
); );
} }
this.logger.attach(this.docker, data.id, { this.logger.attach(data.id, {
serviceId, serviceId,
imageId, imageId,
}); });
@ -487,7 +481,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
`containerId not defined for service: ${service.serviceName} in ServiceManager.attachToRunning`, `containerId not defined for service: ${service.serviceName} in ServiceManager.attachToRunning`,
); );
} }
this.logger.attach(this.docker, service.containerId, { this.logger.attach(service.containerId, {
serviceId, serviceId,
imageId, imageId,
}); });
@ -544,7 +538,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
this.reportNewStatus(containerId, service, 'Stopping'); this.reportNewStatus(containerId, service, 'Stopping');
} }
const containerObj = this.docker.getContainer(containerId); const containerObj = docker.getContainer(containerId);
const killPromise = Bluebird.resolve(containerObj.stop()) const killPromise = Bluebird.resolve(containerObj.stop())
.then(() => { .then(() => {
if (removeContainer) { if (removeContainer) {
@ -605,7 +599,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
labelList: string[], labelList: string[],
): Promise<Dockerode.ContainerInfo[]> { ): Promise<Dockerode.ContainerInfo[]> {
const listWithPrefix = (prefix: string) => const listWithPrefix = (prefix: string) =>
this.docker.listContainers({ docker.listContainers({
all: true, all: true,
filters: { filters: {
label: _.map(labelList, (v) => `${prefix}${v}`), label: _.map(labelList, (v) => `${prefix}${v}`),
@ -627,7 +621,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
`No containerId provided for service ${service.serviceName} in ServiceManager.prepareForHandover. Service: ${service}`, `No containerId provided for service ${service.serviceName} in ServiceManager.prepareForHandover. Service: ${service}`,
); );
} }
const container = this.docker.getContainer(svc.containerId); const container = docker.getContainer(svc.containerId);
await container.update({ RestartPolicy: {} }); await container.update({ RestartPolicy: {} });
return await container.rename({ return await container.rename({
name: `old_${service.serviceName}_${service.imageId}_${service.imageId}_${service.releaseId}`, name: `old_${service.serviceName}_${service.imageId}_${service.imageId}_${service.releaseId}`,

View File

@ -1,10 +1,11 @@
import * as Docker from 'dockerode';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as Path from 'path'; import * as Path from 'path';
import { VolumeInspectInfo } from 'dockerode';
import constants = require('../lib/constants'); import constants = require('../lib/constants');
import { NotFoundError } from '../lib/errors'; import { NotFoundError } from '../lib/errors';
import { safeRename } from '../lib/fs-utils'; import { safeRename } from '../lib/fs-utils';
import { docker } from '../lib/docker-utils';
import * as LogTypes from '../lib/log-types'; import * as LogTypes from '../lib/log-types';
import { defaultLegacyVolume } from '../lib/migration'; import { defaultLegacyVolume } from '../lib/migration';
import Logger from '../logger'; import Logger from '../logger';
@ -12,7 +13,6 @@ import { ResourceRecreationAttemptError } from './errors';
import Volume, { VolumeConfig } from './volume'; import Volume, { VolumeConfig } from './volume';
export interface VolumeMangerConstructOpts { export interface VolumeMangerConstructOpts {
docker: Docker;
logger: Logger; logger: Logger;
} }
@ -22,30 +22,23 @@ export interface VolumeNameOpts {
} }
export class VolumeManager { export class VolumeManager {
private docker: Docker;
private logger: Logger; private logger: Logger;
public constructor(opts: VolumeMangerConstructOpts) { public constructor(opts: VolumeMangerConstructOpts) {
this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
} }
public async get({ name, appId }: VolumeNameOpts): Promise<Volume> { public async get({ name, appId }: VolumeNameOpts): Promise<Volume> {
return Volume.fromDockerVolume( return Volume.fromDockerVolume(
{ docker: this.docker, logger: this.logger }, { logger: this.logger },
await this.docker await docker.getVolume(Volume.generateDockerName(appId, name)).inspect(),
.getVolume(Volume.generateDockerName(appId, name))
.inspect(),
); );
} }
public async getAll(): Promise<Volume[]> { public async getAll(): Promise<Volume[]> {
const volumeInspect = await this.listWithBothLabels(); const volumeInspect = await this.listWithBothLabels();
return volumeInspect.map((inspect) => return volumeInspect.map((inspect) =>
Volume.fromDockerVolume( Volume.fromDockerVolume({ logger: this.logger }, inspect),
{ logger: this.logger, docker: this.docker },
inspect,
),
); );
} }
@ -111,11 +104,10 @@ export class VolumeManager {
): Promise<Volume> { ): Promise<Volume> {
const volume = Volume.fromComposeObject(name, appId, config, { const volume = Volume.fromComposeObject(name, appId, config, {
logger: this.logger, logger: this.logger,
docker: this.docker,
}); });
await this.create(volume); await this.create(volume);
const inspect = await this.docker const inspect = await docker
.getVolume(Volume.generateDockerName(volume.appId, volume.name)) .getVolume(Volume.generateDockerName(volume.appId, volume.name))
.inspect(); .inspect();
@ -139,8 +131,8 @@ export class VolumeManager {
// *all* containers. This means we don't remove // *all* containers. This means we don't remove
// something that's part of a sideloaded container // something that's part of a sideloaded container
const [dockerContainers, dockerVolumes] = await Promise.all([ const [dockerContainers, dockerVolumes] = await Promise.all([
this.docker.listContainers(), docker.listContainers(),
this.docker.listVolumes(), docker.listVolumes(),
]); ]);
const containerVolumes = _(dockerContainers) const containerVolumes = _(dockerContainers)
@ -160,17 +152,15 @@ export class VolumeManager {
// in the target state // in the target state
referencedVolumes, referencedVolumes,
); );
await Promise.all( await Promise.all(volumesToRemove.map((v) => docker.getVolume(v).remove()));
volumesToRemove.map((v) => this.docker.getVolume(v).remove()),
);
} }
private async listWithBothLabels(): Promise<Docker.VolumeInspectInfo[]> { private async listWithBothLabels(): Promise<VolumeInspectInfo[]> {
const [legacyResponse, currentResponse] = await Promise.all([ const [legacyResponse, currentResponse] = await Promise.all([
this.docker.listVolumes({ docker.listVolumes({
filters: { label: ['io.resin.supervised'] }, filters: { label: ['io.resin.supervised'] },
}), }),
this.docker.listVolumes({ docker.listVolumes({
filters: { label: ['io.balena.supervised'] }, filters: { label: ['io.balena.supervised'] },
}), }),
]); ]);

View File

@ -4,6 +4,7 @@ import isEqual = require('lodash/isEqual');
import omitBy = require('lodash/omitBy'); import omitBy = require('lodash/omitBy');
import constants = require('../lib/constants'); import constants = require('../lib/constants');
import { docker } from '../lib/docker-utils';
import { InternalInconsistencyError } from '../lib/errors'; import { InternalInconsistencyError } from '../lib/errors';
import * as LogTypes from '../lib/log-types'; import * as LogTypes from '../lib/log-types';
import { LabelObject } from '../lib/types'; import { LabelObject } from '../lib/types';
@ -12,7 +13,6 @@ import * as ComposeUtils from './utils';
export interface VolumeConstructOpts { export interface VolumeConstructOpts {
logger: Logger; logger: Logger;
docker: Docker;
} }
export interface VolumeConfig { export interface VolumeConfig {
@ -29,7 +29,6 @@ export interface ComposeVolumeConfig {
export class Volume { export class Volume {
private logger: Logger; private logger: Logger;
private docker: Docker;
private constructor( private constructor(
public name: string, public name: string,
@ -38,7 +37,6 @@ export class Volume {
opts: VolumeConstructOpts, opts: VolumeConstructOpts,
) { ) {
this.logger = opts.logger; this.logger = opts.logger;
this.docker = opts.docker;
} }
public static fromDockerVolume( public static fromDockerVolume(
@ -100,7 +98,7 @@ export class Volume {
this.logger.logSystemEvent(LogTypes.createVolume, { this.logger.logSystemEvent(LogTypes.createVolume, {
volume: { name: this.name }, volume: { name: this.name },
}); });
await this.docker.createVolume({ await docker.createVolume({
Name: Volume.generateDockerName(this.appId, this.name), Name: Volume.generateDockerName(this.appId, this.name),
Labels: this.config.labels, Labels: this.config.labels,
Driver: this.config.driver, Driver: this.config.driver,
@ -114,7 +112,7 @@ export class Volume {
}); });
try { try {
await this.docker await docker
.getVolume(Volume.generateDockerName(this.appId, this.name)) .getVolume(Volume.generateDockerName(this.appId, this.name))
.remove(); .remove();
} catch (e) { } catch (e) {

View File

@ -42,20 +42,20 @@ interface ImageNameParts {
// (10 mins) // (10 mins)
const DELTA_TOKEN_TIMEOUT = 10 * 60 * 1000; const DELTA_TOKEN_TIMEOUT = 10 * 60 * 1000;
export class DockerUtils extends DockerToolbelt { export const docker = new Dockerode();
public dockerProgress: DockerProgress; export const dockerToolbelt = new DockerToolbelt(undefined);
export const dockerProgress = new DockerProgress({
dockerToolbelt,
});
public constructor(opts?: Dockerode.DockerOptions) { export async function getRepoAndTag(
super(opts);
this.dockerProgress = new DockerProgress({ dockerToolbelt: this });
}
public async getRepoAndTag(
image: string, image: string,
): Promise<{ repo: string; tag: string }> { ): Promise<{ repo: string; tag: string }> {
const { registry, imageName, tagName } = await this.getRegistryAndName( const {
image, registry,
); imageName,
tagName,
} = await dockerToolbelt.getRegistryAndName(image);
let repoName = imageName; let repoName = imageName;
@ -66,7 +66,7 @@ export class DockerUtils extends DockerToolbelt {
return { repo: repoName, tag: tagName }; return { repo: repoName, tag: tagName };
} }
public async fetchDeltaWithProgress( export async function fetchDeltaWithProgress(
imgDest: string, imgDest: string,
deltaOpts: DeltaFetchOptions, deltaOpts: DeltaFetchOptions,
onProgress: ProgressCallback, onProgress: ProgressCallback,
@ -86,7 +86,7 @@ export class DockerUtils extends DockerToolbelt {
logFn( logFn(
`Unsupported delta version: ${deltaOpts.deltaVersion}. Falling back to regular pull`, `Unsupported delta version: ${deltaOpts.deltaVersion}. Falling back to regular pull`,
); );
return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); return await fetchImageWithProgress(imgDest, deltaOpts, onProgress);
} }
// We need to make sure that we're not trying to apply a // We need to make sure that we're not trying to apply a
@ -95,30 +95,27 @@ export class DockerUtils extends DockerToolbelt {
// image pull // image pull
if ( if (
deltaOpts.deltaVersion === 3 && deltaOpts.deltaVersion === 3 &&
(await DockerUtils.isV2DeltaImage(this, deltaOpts.deltaSourceId)) (await isV2DeltaImage(deltaOpts.deltaSourceId))
) { ) {
logFn( logFn(`Cannot create a delta from V2 to V3, falling back to regular pull`);
`Cannot create a delta from V2 to V3, falling back to regular pull`, return await fetchImageWithProgress(imgDest, deltaOpts, onProgress);
);
return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress);
} }
// Since the supevisor never calls this function with a source anymore, // Since the supevisor never calls this function with a source anymore,
// this should never happen, but w ehandle it anyway // this should never happen, but w ehandle it anyway
if (deltaOpts.deltaSource == null) { if (deltaOpts.deltaSource == null) {
logFn('Falling back to regular pull due to lack of a delta source'); logFn('Falling back to regular pull due to lack of a delta source');
return this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); return fetchImageWithProgress(imgDest, deltaOpts, onProgress);
} }
const docker = this;
logFn(`Starting delta to ${imgDest}`); logFn(`Starting delta to ${imgDest}`);
const [dstInfo, srcInfo] = await Promise.all([ const [dstInfo, srcInfo] = await Promise.all([
this.getRegistryAndName(imgDest), dockerToolbelt.getRegistryAndName(imgDest),
this.getRegistryAndName(deltaOpts.deltaSource), dockerToolbelt.getRegistryAndName(deltaOpts.deltaSource),
]); ]);
const token = await this.getAuthToken(srcInfo, dstInfo, deltaOpts); const token = await getAuthToken(srcInfo, dstInfo, deltaOpts);
const opts: request.requestLib.CoreOptions = { const opts: request.requestLib.CoreOptions = {
followRedirect: false, followRedirect: false,
@ -160,7 +157,7 @@ export class DockerUtils extends DockerToolbelt {
maxRetries: deltaOpts.deltaRetryCount, maxRetries: deltaOpts.deltaRetryCount,
retryInterval: deltaOpts.deltaRetryInterval, retryInterval: deltaOpts.deltaRetryInterval,
}; };
id = await DockerUtils.applyRsyncDelta( id = await applyRsyncDelta(
deltaSrc, deltaSrc,
deltaUrl, deltaUrl,
timeout, timeout,
@ -183,27 +180,15 @@ export class DockerUtils extends DockerToolbelt {
`Got an error when parsing delta server response for v3 delta: ${e}`, `Got an error when parsing delta server response for v3 delta: ${e}`,
); );
} }
id = await DockerUtils.applyBalenaDelta( id = await applyBalenaDelta(name, token, onProgress, logFn);
docker,
name,
token,
onProgress,
logFn,
);
break; break;
default: default:
throw new Error( throw new Error(`Unsupported delta version: ${deltaOpts.deltaVersion}`);
`Unsupported delta version: ${deltaOpts.deltaVersion}`,
);
} }
} catch (e) { } catch (e) {
if (e instanceof OutOfSyncError) { if (e instanceof OutOfSyncError) {
logFn('Falling back to regular pull due to delta out of sync error'); logFn('Falling back to regular pull due to delta out of sync error');
return await this.fetchImageWithProgress( return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress);
imgDest,
deltaOpts,
onProgress,
);
} else { } else {
logFn(`Delta failed with ${e}`); logFn(`Delta failed with ${e}`);
throw e; throw e;
@ -214,12 +199,12 @@ export class DockerUtils extends DockerToolbelt {
return id; return id;
} }
public async fetchImageWithProgress( export async function fetchImageWithProgress(
image: string, image: string,
{ uuid, currentApiKey }: FetchOptions, { uuid, currentApiKey }: FetchOptions,
onProgress: ProgressCallback, onProgress: ProgressCallback,
): Promise<string> { ): Promise<string> {
const { registry } = await this.getRegistryAndName(image); const { registry } = await dockerToolbelt.getRegistryAndName(image);
const dockerOpts = { const dockerOpts = {
authconfig: { authconfig: {
@ -229,11 +214,11 @@ export class DockerUtils extends DockerToolbelt {
}, },
}; };
await this.dockerProgress.pull(image, onProgress, dockerOpts); await dockerProgress.pull(image, onProgress, dockerOpts);
return (await this.getImage(image).inspect()).Id; return (await docker.getImage(image).inspect()).Id;
} }
public async getImageEnv(id: string): Promise<EnvVarObject> { export async function getImageEnv(id: string): Promise<EnvVarObject> {
const inspect = await this.getImage(id).inspect(); const inspect = await this.getImage(id).inspect();
try { try {
@ -244,7 +229,7 @@ export class DockerUtils extends DockerToolbelt {
} }
} }
public async getNetworkGateway(networkName: string): Promise<string> { export async function getNetworkGateway(networkName: string): Promise<string> {
if (networkName === 'host') { if (networkName === 'host') {
return '127.0.0.1'; return '127.0.0.1';
} }
@ -264,7 +249,7 @@ export class DockerUtils extends DockerToolbelt {
); );
} }
private static applyRsyncDelta( function applyRsyncDelta(
imgSrc: string, imgSrc: string,
deltaUrl: string, deltaUrl: string,
applyTimeout: number, applyTimeout: number,
@ -308,8 +293,7 @@ export class DockerUtils extends DockerToolbelt {
}); });
} }
private static async applyBalenaDelta( async function applyBalenaDelta(
docker: DockerUtils,
deltaImg: string, deltaImg: string,
token: string | null, token: string | null,
onProgress: ProgressCallback, onProgress: ProgressCallback,
@ -327,14 +311,11 @@ export class DockerUtils extends DockerToolbelt {
}; };
} }
await docker.dockerProgress.pull(deltaImg, onProgress, auth); await dockerProgress.pull(deltaImg, onProgress, auth);
return (await docker.getImage(deltaImg).inspect()).Id; return (await docker.getImage(deltaImg).inspect()).Id;
} }
public static async isV2DeltaImage( export async function isV2DeltaImage(imageName: string): Promise<boolean> {
docker: DockerUtils,
imageName: string,
): Promise<boolean> {
const inspect = await docker.getImage(imageName).inspect(); const inspect = await docker.getImage(imageName).inspect();
// It's extremely unlikely that an image is valid if // It's extremely unlikely that an image is valid if
@ -344,7 +325,7 @@ export class DockerUtils extends DockerToolbelt {
return inspect.Size < 40 && inspect.VirtualSize < 40; return inspect.Size < 40 && inspect.VirtualSize < 40;
} }
private getAuthToken = memoizee( const getAuthToken = memoizee(
async ( async (
srcInfo: ImageNameParts, srcInfo: ImageNameParts,
dstInfo: ImageNameParts, dstInfo: ImageNameParts,
@ -374,6 +355,3 @@ export class DockerUtils extends DockerToolbelt {
}, },
{ maxAge: DELTA_TOKEN_TIMEOUT, promise: true }, { maxAge: DELTA_TOKEN_TIMEOUT, promise: true },
); );
}
export default DockerUtils;

View File

@ -15,6 +15,7 @@ import * as db from '../db';
import DeviceState from '../device-state'; import DeviceState from '../device-state';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors'; import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors';
import { docker } from '../lib/docker-utils';
import { pathExistsOnHost } from '../lib/fs-utils'; import { pathExistsOnHost } from '../lib/fs-utils';
import { log } from '../lib/supervisor-console'; import { log } from '../lib/supervisor-console';
import { import {
@ -179,7 +180,7 @@ export async function normaliseLegacyDatabase(
`Found a release with releaseId ${release.id}, imageId ${image.id}, serviceId ${serviceId}\nImage location is ${imageUrl}`, `Found a release with releaseId ${release.id}, imageId ${image.id}, serviceId ${serviceId}\nImage location is ${imageUrl}`,
); );
const imageFromDocker = await application.docker const imageFromDocker = await docker
.getImage(service.image) .getImage(service.image)
.inspect() .inspect()
.catch((error) => { .catch((error) => {

View File

@ -1,10 +1,10 @@
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import * as Docker from 'dockerode';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as config from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import { docker } from './lib/docker-utils';
import { SupervisorContainerNotFoundError } from './lib/errors'; import { SupervisorContainerNotFoundError } from './lib/errors';
import log from './lib/supervisor-console'; import log from './lib/supervisor-console';
import { Logger } from './logger'; import { Logger } from './logger';
@ -71,7 +71,6 @@ const SUPERVISOR_CONTAINER_NAME_FALLBACK = 'resin_supervisor';
*/ */
export class LocalModeManager { export class LocalModeManager {
public constructor( public constructor(
public docker: Docker,
public logger: Logger, public logger: Logger,
private containerId: string | undefined = constants.containerId, private containerId: string | undefined = constants.containerId,
) {} ) {}
@ -121,16 +120,14 @@ export class LocalModeManager {
// Query the engine to get currently running containers and installed images. // Query the engine to get currently running containers and installed images.
public async collectEngineSnapshot(): Promise<EngineSnapshotRecord> { public async collectEngineSnapshot(): Promise<EngineSnapshotRecord> {
const containersPromise = this.docker const containersPromise = docker
.listContainers() .listContainers()
.then((resp) => _.map(resp, 'Id')); .then((resp) => _.map(resp, 'Id'));
const imagesPromise = this.docker const imagesPromise = docker.listImages().then((resp) => _.map(resp, 'Id'));
.listImages() const volumesPromise = docker
.then((resp) => _.map(resp, 'Id'));
const volumesPromise = this.docker
.listVolumes() .listVolumes()
.then((resp) => _.map(resp.Volumes, 'Name')); .then((resp) => _.map(resp.Volumes, 'Name'));
const networksPromise = this.docker const networksPromise = docker
.listNetworks() .listNetworks()
.then((resp) => _.map(resp, 'Id')); .then((resp) => _.map(resp, 'Id'));
@ -149,7 +146,7 @@ export class LocalModeManager {
private async collectContainerResources( private async collectContainerResources(
nameOrId: string, nameOrId: string,
): Promise<EngineSnapshot> { ): Promise<EngineSnapshot> {
const inspectInfo = await this.docker.getContainer(nameOrId).inspect(); const inspectInfo = await docker.getContainer(nameOrId).inspect();
return new EngineSnapshot( return new EngineSnapshot(
[inspectInfo.Id], [inspectInfo.Id],
[inspectInfo.Image], [inspectInfo.Image],
@ -236,25 +233,25 @@ export class LocalModeManager {
// Delete engine objects. We catch every deletion error, so that we can attempt other objects deletions. // Delete engine objects. We catch every deletion error, so that we can attempt other objects deletions.
await Bluebird.map(objects.containers, (cId) => { await Bluebird.map(objects.containers, (cId) => {
return this.docker return docker
.getContainer(cId) .getContainer(cId)
.remove({ force: true }) .remove({ force: true })
.catch((e) => log.error(`Unable to delete container ${cId}`, e)); .catch((e) => log.error(`Unable to delete container ${cId}`, e));
}); });
await Bluebird.map(objects.images, (iId) => { await Bluebird.map(objects.images, (iId) => {
return this.docker return docker
.getImage(iId) .getImage(iId)
.remove({ force: true }) .remove({ force: true })
.catch((e) => log.error(`Unable to delete image ${iId}`, e)); .catch((e) => log.error(`Unable to delete image ${iId}`, e));
}); });
await Bluebird.map(objects.networks, (nId) => { await Bluebird.map(objects.networks, (nId) => {
return this.docker return docker
.getNetwork(nId) .getNetwork(nId)
.remove() .remove()
.catch((e) => log.error(`Unable to delete network ${nId}`, e)); .catch((e) => log.error(`Unable to delete network ${nId}`, e));
}); });
await Bluebird.map(objects.volumes, (vId) => { await Bluebird.map(objects.volumes, (vId) => {
return this.docker return docker
.getVolume(vId) .getVolume(vId)
.remove() .remove()
.catch((e) => log.error(`Unable to delete volume ${vId}`, e)); .catch((e) => log.error(`Unable to delete volume ${vId}`, e));

View File

@ -4,7 +4,6 @@ import * as _ from 'lodash';
import * as config from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import Docker from './lib/docker-utils';
import { LogType } from './lib/log-types'; import { LogType } from './lib/log-types';
import { writeLock } from './lib/update-lock'; import { writeLock } from './lib/update-lock';
import { import {
@ -159,7 +158,6 @@ export class Logger {
} }
public attach( public attach(
docker: Docker,
containerId: string, containerId: string,
serviceInfo: { serviceId: number; imageId: number }, serviceInfo: { serviceId: number; imageId: number },
): Bluebird<void> { ): Bluebird<void> {
@ -170,7 +168,7 @@ export class Logger {
} }
return Bluebird.using(this.lock(containerId), async () => { return Bluebird.using(this.lock(containerId), async () => {
const logs = new ContainerLogs(containerId, docker); const logs = new ContainerLogs(containerId);
this.containerLogs[containerId] = logs; this.containerLogs[containerId] = logs;
logs.on('error', (err) => { logs.on('error', (err) => {
log.error('Container log retrieval error', err); log.error('Container log retrieval error', err);

View File

@ -4,7 +4,7 @@ import * as _ from 'lodash';
import * as Stream from 'stream'; import * as Stream from 'stream';
import StrictEventEmitter from 'strict-event-emitter-types'; import StrictEventEmitter from 'strict-event-emitter-types';
import Docker from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
export interface ContainerLog { export interface ContainerLog {
message: string; message: string;
@ -21,7 +21,7 @@ interface LogsEvents {
type LogsEventEmitter = StrictEventEmitter<EventEmitter, LogsEvents>; type LogsEventEmitter = StrictEventEmitter<EventEmitter, LogsEvents>;
export class ContainerLogs extends (EventEmitter as new () => LogsEventEmitter) { export class ContainerLogs extends (EventEmitter as new () => LogsEventEmitter) {
public constructor(public containerId: string, private docker: Docker) { public constructor(public containerId: string) {
super(); super();
} }
@ -34,7 +34,7 @@ export class ContainerLogs extends (EventEmitter as new () => LogsEventEmitter)
const stdoutLogOpts = { stdout: true, stderr: false, ...logOpts }; const stdoutLogOpts = { stdout: true, stderr: false, ...logOpts };
const stderrLogOpts = { stderr: true, stdout: false, ...logOpts }; const stderrLogOpts = { stderr: true, stdout: false, ...logOpts };
const container = this.docker.getContainer(this.containerId); const container = docker.getContainer(this.containerId);
const stdoutStream = await container.logs(stdoutLogOpts); const stdoutStream = await container.logs(stdoutLogOpts);
const stderrStream = await container.logs(stderrLogOpts); const stderrStream = await container.logs(stderrLogOpts);

View File

@ -17,6 +17,7 @@ import * as url from 'url';
import { log } from './lib/supervisor-console'; import { log } from './lib/supervisor-console';
import * as db from './db'; import * as db from './db';
import * as config from './config'; import * as config from './config';
import * as dockerUtils from './lib/docker-utils';
const mkdirpAsync = Promise.promisify(mkdirp); const mkdirpAsync = Promise.promisify(mkdirp);
@ -344,7 +345,7 @@ const createProxyvisorRouter = function (proxyvisor) {
}; };
export class Proxyvisor { export class Proxyvisor {
constructor({ logger, docker, images, applications }) { constructor({ logger, images, applications }) {
this.bindToAPI = this.bindToAPI.bind(this); this.bindToAPI = this.bindToAPI.bind(this);
this.executeStepAction = this.executeStepAction.bind(this); this.executeStepAction = this.executeStepAction.bind(this);
this.getCurrentStates = this.getCurrentStates.bind(this); this.getCurrentStates = this.getCurrentStates.bind(this);
@ -361,7 +362,6 @@ export class Proxyvisor {
this.sendDeleteHook = this.sendDeleteHook.bind(this); this.sendDeleteHook = this.sendDeleteHook.bind(this);
this.sendUpdates = this.sendUpdates.bind(this); this.sendUpdates = this.sendUpdates.bind(this);
this.logger = logger; this.logger = logger;
this.docker = docker;
this.images = images; this.images = images;
this.applications = applications; this.applications = applications;
this.acknowledgedState = {}; this.acknowledgedState = {};
@ -904,7 +904,7 @@ export class Proxyvisor {
}) })
.then((parentApp) => { .then((parentApp) => {
return Promise.map(parentApp?.services ?? [], (service) => { return Promise.map(parentApp?.services ?? [], (service) => {
return this.docker.getImageEnv(service.image); return dockerUtils.getImageEnv(service.image);
}).then(function (imageEnvs) { }).then(function (imageEnvs) {
const imageHookAddresses = _.map( const imageHookAddresses = _.map(
imageEnvs, imageEnvs,

View File

@ -6,6 +6,7 @@ import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
import Log from '../src/lib/supervisor-console'; import Log from '../src/lib/supervisor-console';
import * as dockerUtils from '../src/lib/docker-utils';
import * as config from '../src/config'; import * as config from '../src/config';
import { RPiConfigBackend } from '../src/config/backend'; import { RPiConfigBackend } from '../src/config/backend';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
@ -235,7 +236,7 @@ describe('deviceState', () => {
apiBinder: null as any, apiBinder: null as any,
}); });
stub(deviceState.applications.docker, 'getNetworkGateway').returns( stub(dockerUtils, 'getNetworkGateway').returns(
Promise.resolve('172.17.0.1'), Promise.resolve('172.17.0.1'),
); );
@ -250,8 +251,7 @@ describe('deviceState', () => {
after(() => { after(() => {
(Service as any).extendEnvVars.restore(); (Service as any).extendEnvVars.restore();
(deviceState.applications.docker (dockerUtils.getNetworkGateway as sinon.SinonStub).restore();
.getNetworkGateway as sinon.SinonStub).restore();
(deviceState.applications.images (deviceState.applications.images
.inspectByName as sinon.SinonStub).restore(); .inspectByName as sinon.SinonStub).restore();
}); });

View File

@ -8,6 +8,7 @@ import Service from '../src/compose/service';
import Volume from '../src/compose/volume'; import Volume from '../src/compose/volume';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
import EventTracker from '../src/event-tracker'; import EventTracker from '../src/event-tracker';
import * as dockerUtils from '../src/lib/docker-utils';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
@ -148,13 +149,13 @@ describe('ApplicationManager', function () {
}, },
}), }),
); );
stub(this.applications.docker, 'getNetworkGateway').returns( stub(dockerUtils, 'getNetworkGateway').returns(
Bluebird.Promise.resolve('172.17.0.1'), Bluebird.Promise.resolve('172.17.0.1'),
); );
stub(this.applications.docker, 'listContainers').returns( stub(dockerUtils.docker, 'listContainers').returns(
Bluebird.Promise.resolve([]), Bluebird.Promise.resolve([]),
); );
stub(this.applications.docker, 'listImages').returns( stub(dockerUtils.docker, 'listImages').returns(
Bluebird.Promise.resolve([]), Bluebird.Promise.resolve([]),
); );
stub(Service as any, 'extendEnvVars').callsFake(function (env) { stub(Service as any, 'extendEnvVars').callsFake(function (env) {
@ -174,7 +175,6 @@ describe('ApplicationManager', function () {
appCloned.networks, appCloned.networks,
(config, name) => { (config, name) => {
return Network.fromComposeObject(name, app.appId, config, { return Network.fromComposeObject(name, app.appId, config, {
docker: this.applications.docker,
logger: this.logger, logger: this.logger,
}); });
}, },
@ -235,8 +235,12 @@ describe('ApplicationManager', function () {
after(function () { after(function () {
this.applications.images.inspectByName.restore(); this.applications.images.inspectByName.restore();
this.applications.docker.getNetworkGateway.restore(); // @ts-expect-error restore on non-stubbed type
this.applications.docker.listContainers.restore(); dockerUtils.getNetworkGateway.restore();
// @ts-expect-error restore on non-stubbed type
dockerUtils.docker.listContainers.restore();
// @ts-expect-error restore on non-stubbed type
dockerUtils.docker.listImages.restore();
return (Service as any).extendEnvVars.restore(); return (Service as any).extendEnvVars.restore();
}); });

View File

@ -4,7 +4,7 @@ import APIBinder from '../src/api-binder';
import { ApplicationManager } from '../src/application-manager'; import { ApplicationManager } from '../src/application-manager';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
import * as constants from '../src/lib/constants'; import * as constants from '../src/lib/constants';
import { DockerUtils as Docker } from '../src/lib/docker-utils'; import { docker } from '../src/lib/docker-utils';
import { Supervisor } from '../src/supervisor'; import { Supervisor } from '../src/supervisor';
import { expect } from './lib/chai-config'; import { expect } from './lib/chai-config';
@ -30,9 +30,7 @@ describe('Startup', () => {
deviceStateStub = stub(DeviceState.prototype as any, 'applyTarget').returns( deviceStateStub = stub(DeviceState.prototype as any, 'applyTarget').returns(
Promise.resolve(), Promise.resolve(),
); );
dockerStub = stub(Docker.prototype, 'listContainers').returns( dockerStub = stub(docker, 'listContainers').returns(Promise.resolve([]));
Promise.resolve([]),
);
}); });
after(() => { after(() => {

View File

@ -1,5 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { stub } from 'sinon'; import { stub, SinonStub } from 'sinon';
import { docker } from '../src/lib/docker-utils';
import Volume from '../src/compose/volume'; import Volume from '../src/compose/volume';
import logTypes = require('../src/lib/log-types'); import logTypes = require('../src/lib/log-types');
@ -8,13 +10,18 @@ const fakeLogger = {
logSystemMessage: stub(), logSystemMessage: stub(),
logSystemEvent: stub(), logSystemEvent: stub(),
}; };
const fakeDocker = {
createVolume: stub(),
};
const opts: any = { logger: fakeLogger, docker: fakeDocker }; const opts: any = { logger: fakeLogger };
describe('Compose volumes', () => { describe('Compose volumes', () => {
let createVolumeStub: SinonStub;
before(() => {
createVolumeStub = stub(docker, 'createVolume');
});
after(() => {
createVolumeStub.restore();
});
describe('Parsing volumes', () => { describe('Parsing volumes', () => {
it('should correctly parse docker volumes', () => { it('should correctly parse docker volumes', () => {
const volume = Volume.fromDockerVolume(opts, { const volume = Volume.fromDockerVolume(opts, {
@ -122,7 +129,7 @@ describe('Compose volumes', () => {
describe('Generating docker options', () => { describe('Generating docker options', () => {
afterEach(() => { afterEach(() => {
fakeDocker.createVolume.reset(); createVolumeStub.reset();
fakeLogger.logSystemEvent.reset(); fakeLogger.logSystemEvent.reset();
fakeLogger.logSystemMessage.reset(); fakeLogger.logSystemMessage.reset();
}); });
@ -143,7 +150,7 @@ describe('Compose volumes', () => {
await volume.create(); await volume.create();
expect( expect(
fakeDocker.createVolume.calledWith({ createVolumeStub.calledWith({
Labels: { Labels: {
'my-label': 'test-label', 'my-label': 'test-label',
'io.balena.supervised': 'true', 'io.balena.supervised': 'true',

View File

@ -4,6 +4,7 @@ import * as Docker from 'dockerode';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as db from '../src/db'; import * as db from '../src/db';
import { docker } from '../src/lib/docker-utils';
import LocalModeManager, { import LocalModeManager, {
EngineSnapshot, EngineSnapshot,
EngineSnapshotRecord, EngineSnapshotRecord,
@ -13,7 +14,7 @@ import ShortStackError from './lib/errors';
describe('LocalModeManager', () => { describe('LocalModeManager', () => {
let localMode: LocalModeManager; let localMode: LocalModeManager;
let dockerStub: sinon.SinonStubbedInstance<Docker>; let dockerStub: sinon.SinonStubbedInstance<typeof docker>;
const supervisorContainerId = 'super-container-1'; const supervisorContainerId = 'super-container-1';
@ -32,14 +33,14 @@ describe('LocalModeManager', () => {
before(async () => { before(async () => {
await db.initialized; await db.initialized;
dockerStub = sinon.createStubInstance(Docker); dockerStub = sinon.stub(docker);
const loggerStub = (sinon.createStubInstance(Logger) as unknown) as Logger; const loggerStub = (sinon.createStubInstance(Logger) as unknown) as Logger;
localMode = new LocalModeManager( localMode = new LocalModeManager(loggerStub, supervisorContainerId);
(dockerStub as unknown) as Docker, });
loggerStub,
supervisorContainerId, after(async () => {
); sinon.restore();
}); });
describe('EngineSnapshot', () => { describe('EngineSnapshot', () => {
@ -427,8 +428,4 @@ describe('LocalModeManager', () => {
}); });
}); });
}); });
after(async () => {
sinon.restore();
});
}); });

View File

@ -1,13 +1,11 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { stub } from 'sinon'; import { stub } from 'sinon';
import DockerUtils from '../src/lib/docker-utils'; import * as dockerUtils from '../src/lib/docker-utils';
const dockerUtils = new DockerUtils({});
describe('Deltas', () => { describe('Deltas', () => {
it('should correctly detect a V2 delta', async () => { it('should correctly detect a V2 delta', async () => {
const imageStub = stub(dockerUtils, 'getImage').returns({ const imageStub = stub(dockerUtils.docker, 'getImage').returns({
inspect: () => { inspect: () => {
return Promise.resolve({ return Promise.resolve({
Id: Id:
@ -99,7 +97,7 @@ describe('Deltas', () => {
}, },
} as any); } as any);
expect(await DockerUtils.isV2DeltaImage(dockerUtils, 'test')).to.be.true; expect(await dockerUtils.isV2DeltaImage('test')).to.be.true;
expect(imageStub.callCount).to.equal(1); expect(imageStub.callCount).to.equal(1);
imageStub.restore(); imageStub.restore();
}); });