diff --git a/src/application-manager.d.ts b/src/application-manager.d.ts index 6d733721..d7e164ab 100644 --- a/src/application-manager.d.ts +++ b/src/application-manager.d.ts @@ -25,7 +25,6 @@ import { import Network from './compose/network'; import Service from './compose/service'; import Volume from './compose/volume'; -import DockerUtils from './lib/docker-utils'; declare interface Options { force?: boolean; @@ -52,7 +51,6 @@ class ApplicationManager extends EventEmitter { public deviceState: DeviceState; public eventTracker: EventTracker; public apiBinder: APIBinder; - public docker: DockerUtils; public services: ServiceManager; public volumes: VolumeManager; diff --git a/src/application-manager.js b/src/application-manager.js index 2137bdf4..38212059 100644 --- a/src/application-manager.js +++ b/src/application-manager.js @@ -11,7 +11,8 @@ import { log } from './lib/supervisor-console'; import * as config from './config'; 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 * as updateLock from './lib/update-lock'; import { checkTruthy, checkInt, checkString } from './lib/validation'; @@ -172,30 +173,24 @@ export class ApplicationManager extends EventEmitter { this.eventTracker = eventTracker; this.deviceState = deviceState; this.apiBinder = apiBinder; - this.docker = new Docker(); this.images = new Images({ - docker: this.docker, logger: this.logger, }); this.services = new ServiceManager({ - docker: this.docker, logger: this.logger, }); this.networks = new NetworkManager({ - docker: this.docker, logger: this.logger, }); this.volumes = new VolumeManager({ - docker: this.docker, logger: this.logger, }); this.proxyvisor = new Proxyvisor({ logger: this.logger, - docker: this.docker, images: this.images, applications: this, }); - this.localModeManager = new LocalModeManager(this.docker, this.logger); + this.localModeManager = new LocalModeManager(this.logger); this.timeSpentFetching = 0; this.fetchesInProgress = 0; this._targetVolatilePerImageId = {}; @@ -257,11 +252,9 @@ export class ApplicationManager extends EventEmitter { }) .then(() => { const cleanup = () => { - return this.docker - .listContainers({ all: true }) - .then((containers) => { - return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id')); - }); + return docker.listContainers({ all: true }).then((containers) => { + return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id')); + }); }; // Rather than relying on removing out of date database entries when we're no // longer using them, set a task that runs periodically to clear out the database @@ -1061,14 +1054,12 @@ export class ApplicationManager extends EventEmitter { createTargetVolume(name, appId, volume) { return Volume.fromComposeObject(name, appId, volume, { - docker: this.docker, logger: this.logger, }); } createTargetNetwork(name, appId, network) { return Network.fromComposeObject(name, appId, network, { - docker: this.docker, logger: this.logger, }); } @@ -1076,7 +1067,7 @@ export class ApplicationManager extends EventEmitter { normaliseAndExtendAppFromDB(app) { return Promise.join( config.get('extendedEnvOptions'), - this.docker + dockerUtils .getNetworkGateway(constants.supervisorNetworkInterface) .catch(() => '127.0.0.1'), Promise.props({ diff --git a/src/compose/images.ts b/src/compose/images.ts index a77fc10f..9e92905c 100644 --- a/src/compose/images.ts +++ b/src/compose/images.ts @@ -8,9 +8,11 @@ import * as db from '../db'; import * as constants from '../lib/constants'; import { DeltaFetchOptions, - DockerUtils, FetchOptions, + docker, + dockerToolbelt, } from '../lib/docker-utils'; +import * as dockerUtils from '../lib/docker-utils'; import { DeltaStillProcessingError, NotFoundError } from '../lib/errors'; import * as LogTypes from '../lib/log-types'; import * as validation from '../lib/validation'; @@ -26,7 +28,6 @@ interface ImageEvents { type ImageEventEmitter = StrictEventEmitter; interface ImageConstructOpts { - docker: DockerUtils; logger: Logger; } @@ -56,7 +57,6 @@ type NormalisedDockerImage = Docker.ImageInfo & { }; export class Images extends (EventEmitter as new () => ImageEventEmitter) { - private docker: DockerUtils; private logger: Logger; public appUpdatePollInterval: number; @@ -72,7 +72,6 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { public constructor(opts: ImageConstructOpts) { super(); - this.docker = opts.docker; this.logger = opts.logger; } @@ -202,7 +201,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { cb: (dockerImages: NormalisedDockerImage[], composeImages: Image[]) => T, ) { 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; newImage.NormalisedRepoTags = await this.getNormalisedTags(image); return newImage; @@ -337,8 +336,8 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { supervisorImage, usedImageIds, ] = await Promise.all([ - this.docker.getRegistryAndName(constants.supervisorImage), - this.docker.getImage(constants.supervisorImage).inspect(), + dockerToolbelt.getRegistryAndName(constants.supervisorImage), + docker.getImage(constants.supervisorImage).inspect(), db .models('image') .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) { // Cleanup should remove truly dangling images (i.e dangling and with no digests) 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 for (const tag of image.RepoTags) { - const imageNameComponents = await this.docker.getRegistryAndName(tag); + const imageNameComponents = await dockerToolbelt.getRegistryAndName( + tag, + ); if (isSupervisorRepoTag(imageNameComponents)) { images.push(image.Id); } @@ -400,7 +401,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { imageName: string, ): Promise { try { - return await this.docker.getImage(imageName).inspect(); + return await docker.getImage(imageName).inspect(); } catch (e) { if (NotFoundError(e)) { const digest = imageName.split('@')[1]; @@ -418,7 +419,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { for (const image of imagesFromDb) { 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) { log.debug(`Cleaning up ${image}`); try { - await this.docker.getImage(image).remove({ force: true }); + await docker.getImage(image).remove({ force: true }); delete this.imageCleanupFailures[image]; } catch (e) { this.logger.logSystemMessage( @@ -459,7 +460,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { } public normalise(imageName: string): Bluebird { - return this.docker.normaliseImageName(imageName); + return dockerToolbelt.normaliseImageName(imageName); } private static isDangling(image: Docker.ImageInfo): boolean { @@ -496,7 +497,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { if (img.dockerImageId == null) { // Legacy image from before we started using dockerImageId, so we try to remove it // by name - await this.docker.getImage(img.name).remove({ force: true }); + await docker.getImage(img.name).remove({ force: true }); removed = true; } else { const imagesFromDb = await db @@ -512,11 +513,11 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { _.merge(_.clone(image), { status: 'Deleting' }), ); this.logger.logSystemEvent(LogTypes.deleteImage, { image }); - this.docker.getImage(img.dockerImageId).remove({ force: true }); + docker.getImage(img.dockerImageId).remove({ force: true }); removed = true; } else if (!Images.hasDigest(img.name)) { // 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) .inspect(); const differentTags = _.reject(imagesFromDb, { name: img.name }); @@ -528,7 +529,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { _.some(differentTags, { name: t }), ) ) { - await this.docker.getImage(img.name).remove({ noprune: true }); + await docker.getImage(img.name).remove({ noprune: true }); } removed = false; } else { @@ -589,7 +590,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { const srcImage = await this.inspectByName(deltaOpts.deltaSource); deltaOpts.deltaSourceId = srcImage.Id; - const id = await this.docker.fetchDeltaWithProgress( + const id = await dockerUtils.fetchDeltaWithProgress( image.name, deltaOpts, onProgress, @@ -597,8 +598,8 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { ); if (!Images.hasDigest(image.name)) { - const { repo, tag } = await this.docker.getRepoAndTag(image.name); - await this.docker.getImage(id).tag({ repo, tag }); + const { repo, tag } = await dockerUtils.getRepoAndTag(image.name); + await docker.getImage(id).tag({ repo, tag }); } return id; @@ -610,7 +611,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) { onProgress: (evt: FetchProgressEvent) => void, ): Promise { 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 diff --git a/src/compose/network-manager.ts b/src/compose/network-manager.ts index 724b14af..f9f88046 100644 --- a/src/compose/network-manager.ts +++ b/src/compose/network-manager.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import { fs } from 'mz'; 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 logTypes = require('../lib/log-types'); import { Logger } from '../logger'; @@ -13,23 +13,20 @@ import log from '../lib/supervisor-console'; import { ResourceRecreationAttemptError } from './errors'; export class NetworkManager { - private docker: Docker; private logger: Logger; constructor(opts: NetworkOptions) { - this.docker = opts.docker; this.logger = opts.logger; } public getAll(): Bluebird { return this.getWithBothLabels().map((network: { Name: string }) => { - return this.docker + return docker .getNetwork(network.Name) .inspect() .then((net) => { return Network.fromDockerNetwork( { - docker: this.docker, logger: this.logger, }, net, @@ -43,13 +40,10 @@ export class NetworkManager { } public async get(network: { name: string; appId: number }): Promise { - const dockerNet = await this.docker + const dockerNet = await docker .getNetwork(Network.generateDockerName(network.appId, network.name)) .inspect(); - return Network.fromDockerNetwork( - { docker: this.docker, logger: this.logger }, - dockerNet, - ); + return Network.fromDockerNetwork({ logger: this.logger }, dockerNet); } public async create(network: Network) { @@ -89,7 +83,7 @@ export class NetworkManager { fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`), ) .then(() => { - return this.docker + return docker .getNetwork(constants.supervisorNetworkInterface) .inspect(); }) @@ -108,16 +102,16 @@ export class NetworkManager { public ensureSupervisorNetwork(): Bluebird { const removeIt = () => { return Bluebird.resolve( - this.docker.getNetwork(constants.supervisorNetworkInterface).remove(), + docker.getNetwork(constants.supervisorNetworkInterface).remove(), ).then(() => { - return this.docker + return docker .getNetwork(constants.supervisorNetworkInterface) .inspect(); }); }; return Bluebird.resolve( - this.docker.getNetwork(constants.supervisorNetworkInterface).inspect(), + docker.getNetwork(constants.supervisorNetworkInterface).inspect(), ) .then((net) => { if ( @@ -138,7 +132,7 @@ export class NetworkManager { .catch(NotFoundError, () => { log.debug(`Creating ${constants.supervisorNetworkInterface} network`); return Bluebird.resolve( - this.docker.createNetwork({ + docker.createNetwork({ Name: constants.supervisorNetworkInterface, Options: { 'com.docker.network.bridge.name': @@ -160,12 +154,12 @@ export class NetworkManager { private getWithBothLabels() { return Bluebird.join( - this.docker.listNetworks({ + docker.listNetworks({ filters: { label: ['io.resin.supervised'], }, }), - this.docker.listNetworks({ + docker.listNetworks({ filters: { label: ['io.balena.supervised'], }, diff --git a/src/compose/network.ts b/src/compose/network.ts index 910eb42f..b922c60c 100644 --- a/src/compose/network.ts +++ b/src/compose/network.ts @@ -1,7 +1,7 @@ import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; -import Docker from '../lib/docker-utils'; +import { docker } from '../lib/docker-utils'; import { InvalidAppIdError } from '../lib/errors'; import logTypes = require('../lib/log-types'); import { checkInt } from '../lib/validation'; @@ -22,7 +22,6 @@ import { } from './errors'; export interface NetworkOptions { - docker: Docker; logger: Logger; } @@ -31,11 +30,9 @@ export class Network { public name: string; public config: NetworkConfig; - private docker: Docker; private logger: Logger; private constructor(opts: NetworkOptions) { - this.docker = opts.docker; this.logger = opts.logger; } @@ -145,7 +142,7 @@ export class Network { network: { name: this.name }, }); - return await this.docker.createNetwork(this.toDockerConfig()); + return await docker.createNetwork(this.toDockerConfig()); } public toDockerConfig(): DockerNetworkConfig { @@ -191,7 +188,7 @@ export class Network { }); return Bluebird.resolve( - this.docker + docker .getNetwork(Network.generateDockerName(this.appId, this.name)) .remove(), ).tapCatch((error) => { diff --git a/src/compose/service-manager.ts b/src/compose/service-manager.ts index b42de5df..055ceb15 100644 --- a/src/compose/service-manager.ts +++ b/src/compose/service-manager.ts @@ -8,7 +8,7 @@ import { fs } from 'mz'; import StrictEventEmitter from 'strict-event-emitter-types'; import * as config from '../config'; -import Docker from '../lib/docker-utils'; +import { docker } from '../lib/docker-utils'; import Logger from '../logger'; import { PermissiveNumber } from '../config/types'; @@ -26,7 +26,6 @@ import { serviceNetworksToDockerNetworks } from './utils'; import log from '../lib/supervisor-console'; interface ServiceConstructOpts { - docker: Docker; logger: Logger; } @@ -44,7 +43,6 @@ interface KillOpts { } export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) { - private docker: Docker; private logger: Logger; // Whether a container has died, indexed by ID @@ -56,7 +54,6 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve public constructor(opts: ServiceConstructOpts) { super(); - this.docker = opts.docker; this.logger = opts.logger; } @@ -68,7 +65,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve const services = await Bluebird.map(containers, async (container) => { try { - const serviceInspect = await this.docker + const serviceInspect = await docker .getContainer(container.Id) .inspect(); const service = Service.fromDockerContainer(serviceInspect); @@ -138,7 +135,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve public async getByDockerContainerId( containerId: string, ): Promise { - const container = await this.docker.getContainer(containerId).inspect(); + const container = await docker.getContainer(containerId).inspect(); if ( container.Config.Labels['io.balena.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}`, }); } @@ -180,10 +177,10 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve // Containers haven't been normalized (this is an updated supervisor) // so we need to stop and remove them const supervisorImageId = ( - await this.docker.getImage(constants.supervisorImage).inspect() + await docker.getImage(constants.supervisorImage).inspect() ).Id; - for (const container of await this.docker.listContainers({ all: true })) { + for (const container of await docker.listContainers({ all: true })) { if (container.ImageID !== supervisorImageId) { await this.killContainer(container.Id, { serviceName: 'legacy', @@ -212,7 +209,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve } try { - await this.docker + await docker .getContainer(existingService.containerId) .remove({ v: true }); } 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}`, ); } - return this.docker.getContainer(existing.containerId); + return docker.getContainer(existing.containerId); } catch (e) { if (!NotFoundError(e)) { this.logger.logSystemEvent(LogTypes.installServiceError, { @@ -280,12 +277,12 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve this.logger.logSystemEvent(LogTypes.installService, { service }); this.reportNewStatus(mockContainerId, service, 'Installing'); - const container = await this.docker.createContainer(conf); + const container = await docker.createContainer(conf); service.containerId = container.id; await Promise.all( _.map((nets || {}).EndpointsConfig, (endpointConfig, name) => - this.docker.getNetwork(name).connect({ + docker.getNetwork(name).connect({ Container: container.id, 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) { this.logger.logSystemEvent(LogTypes.startServiceSuccess, { service }); @@ -389,10 +386,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve this.listening = true; const listen = async () => { - const stream = await this.docker.getEvents({ - // Remove the as any once - // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/43100 - // is merged and released + const stream = await docker.getEvents({ 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`, ); } - this.logger.attach(this.docker, data.id, { + this.logger.attach(data.id, { serviceId, imageId, }); @@ -487,7 +481,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve `containerId not defined for service: ${service.serviceName} in ServiceManager.attachToRunning`, ); } - this.logger.attach(this.docker, service.containerId, { + this.logger.attach(service.containerId, { serviceId, imageId, }); @@ -544,7 +538,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve this.reportNewStatus(containerId, service, 'Stopping'); } - const containerObj = this.docker.getContainer(containerId); + const containerObj = docker.getContainer(containerId); const killPromise = Bluebird.resolve(containerObj.stop()) .then(() => { if (removeContainer) { @@ -605,7 +599,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve labelList: string[], ): Promise { const listWithPrefix = (prefix: string) => - this.docker.listContainers({ + docker.listContainers({ all: true, filters: { 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}`, ); } - const container = this.docker.getContainer(svc.containerId); + const container = docker.getContainer(svc.containerId); await container.update({ RestartPolicy: {} }); return await container.rename({ name: `old_${service.serviceName}_${service.imageId}_${service.imageId}_${service.releaseId}`, diff --git a/src/compose/volume-manager.ts b/src/compose/volume-manager.ts index 65c87295..919691a9 100644 --- a/src/compose/volume-manager.ts +++ b/src/compose/volume-manager.ts @@ -1,10 +1,11 @@ -import * as Docker from 'dockerode'; import * as _ from 'lodash'; import * as Path from 'path'; +import { VolumeInspectInfo } from 'dockerode'; import constants = require('../lib/constants'); import { NotFoundError } from '../lib/errors'; import { safeRename } from '../lib/fs-utils'; +import { docker } from '../lib/docker-utils'; import * as LogTypes from '../lib/log-types'; import { defaultLegacyVolume } from '../lib/migration'; import Logger from '../logger'; @@ -12,7 +13,6 @@ import { ResourceRecreationAttemptError } from './errors'; import Volume, { VolumeConfig } from './volume'; export interface VolumeMangerConstructOpts { - docker: Docker; logger: Logger; } @@ -22,30 +22,23 @@ export interface VolumeNameOpts { } export class VolumeManager { - private docker: Docker; private logger: Logger; public constructor(opts: VolumeMangerConstructOpts) { - this.docker = opts.docker; this.logger = opts.logger; } public async get({ name, appId }: VolumeNameOpts): Promise { return Volume.fromDockerVolume( - { docker: this.docker, logger: this.logger }, - await this.docker - .getVolume(Volume.generateDockerName(appId, name)) - .inspect(), + { logger: this.logger }, + await docker.getVolume(Volume.generateDockerName(appId, name)).inspect(), ); } public async getAll(): Promise { const volumeInspect = await this.listWithBothLabels(); return volumeInspect.map((inspect) => - Volume.fromDockerVolume( - { logger: this.logger, docker: this.docker }, - inspect, - ), + Volume.fromDockerVolume({ logger: this.logger }, inspect), ); } @@ -111,11 +104,10 @@ export class VolumeManager { ): Promise { const volume = Volume.fromComposeObject(name, appId, config, { logger: this.logger, - docker: this.docker, }); await this.create(volume); - const inspect = await this.docker + const inspect = await docker .getVolume(Volume.generateDockerName(volume.appId, volume.name)) .inspect(); @@ -139,8 +131,8 @@ export class VolumeManager { // *all* containers. This means we don't remove // something that's part of a sideloaded container const [dockerContainers, dockerVolumes] = await Promise.all([ - this.docker.listContainers(), - this.docker.listVolumes(), + docker.listContainers(), + docker.listVolumes(), ]); const containerVolumes = _(dockerContainers) @@ -160,17 +152,15 @@ export class VolumeManager { // in the target state referencedVolumes, ); - await Promise.all( - volumesToRemove.map((v) => this.docker.getVolume(v).remove()), - ); + await Promise.all(volumesToRemove.map((v) => docker.getVolume(v).remove())); } - private async listWithBothLabels(): Promise { + private async listWithBothLabels(): Promise { const [legacyResponse, currentResponse] = await Promise.all([ - this.docker.listVolumes({ + docker.listVolumes({ filters: { label: ['io.resin.supervised'] }, }), - this.docker.listVolumes({ + docker.listVolumes({ filters: { label: ['io.balena.supervised'] }, }), ]); diff --git a/src/compose/volume.ts b/src/compose/volume.ts index 3e3706df..261107ef 100644 --- a/src/compose/volume.ts +++ b/src/compose/volume.ts @@ -4,6 +4,7 @@ import isEqual = require('lodash/isEqual'); import omitBy = require('lodash/omitBy'); import constants = require('../lib/constants'); +import { docker } from '../lib/docker-utils'; import { InternalInconsistencyError } from '../lib/errors'; import * as LogTypes from '../lib/log-types'; import { LabelObject } from '../lib/types'; @@ -12,7 +13,6 @@ import * as ComposeUtils from './utils'; export interface VolumeConstructOpts { logger: Logger; - docker: Docker; } export interface VolumeConfig { @@ -29,7 +29,6 @@ export interface ComposeVolumeConfig { export class Volume { private logger: Logger; - private docker: Docker; private constructor( public name: string, @@ -38,7 +37,6 @@ export class Volume { opts: VolumeConstructOpts, ) { this.logger = opts.logger; - this.docker = opts.docker; } public static fromDockerVolume( @@ -100,7 +98,7 @@ export class Volume { this.logger.logSystemEvent(LogTypes.createVolume, { volume: { name: this.name }, }); - await this.docker.createVolume({ + await docker.createVolume({ Name: Volume.generateDockerName(this.appId, this.name), Labels: this.config.labels, Driver: this.config.driver, @@ -114,7 +112,7 @@ export class Volume { }); try { - await this.docker + await docker .getVolume(Volume.generateDockerName(this.appId, this.name)) .remove(); } catch (e) { diff --git a/src/lib/docker-utils.ts b/src/lib/docker-utils.ts index c05c9699..5ff7b649 100644 --- a/src/lib/docker-utils.ts +++ b/src/lib/docker-utils.ts @@ -42,338 +42,316 @@ interface ImageNameParts { // (10 mins) const DELTA_TOKEN_TIMEOUT = 10 * 60 * 1000; -export class DockerUtils extends DockerToolbelt { - public dockerProgress: DockerProgress; +export const docker = new Dockerode(); +export const dockerToolbelt = new DockerToolbelt(undefined); +export const dockerProgress = new DockerProgress({ + dockerToolbelt, +}); - public constructor(opts?: Dockerode.DockerOptions) { - super(opts); - this.dockerProgress = new DockerProgress({ dockerToolbelt: this }); +export async function getRepoAndTag( + image: string, +): Promise<{ repo: string; tag: string }> { + const { + registry, + imageName, + tagName, + } = await dockerToolbelt.getRegistryAndName(image); + + let repoName = imageName; + + if (registry != null) { + repoName = `${registry}/${imageName}`; } - public async getRepoAndTag( - image: string, - ): Promise<{ repo: string; tag: string }> { - const { registry, imageName, tagName } = await this.getRegistryAndName( - image, + return { repo: repoName, tag: tagName }; +} + +export async function fetchDeltaWithProgress( + imgDest: string, + deltaOpts: DeltaFetchOptions, + onProgress: ProgressCallback, + serviceName: string, +): Promise { + const deltaSourceId = + deltaOpts.deltaSourceId != null + ? deltaOpts.deltaSourceId + : deltaOpts.deltaSource; + + const timeout = deltaOpts.deltaApplyTimeout; + + const logFn = (str: string) => + log.debug(`delta([${serviceName}] ${deltaOpts.deltaSource}): ${str}`); + + if (!_.includes([2, 3], deltaOpts.deltaVersion)) { + logFn( + `Unsupported delta version: ${deltaOpts.deltaVersion}. Falling back to regular pull`, ); - - let repoName = imageName; - - if (registry != null) { - repoName = `${registry}/${imageName}`; - } - - return { repo: repoName, tag: tagName }; + return await fetchImageWithProgress(imgDest, deltaOpts, onProgress); } - public async fetchDeltaWithProgress( - imgDest: string, - deltaOpts: DeltaFetchOptions, - onProgress: ProgressCallback, - serviceName: string, - ): Promise { - const deltaSourceId = - deltaOpts.deltaSourceId != null - ? deltaOpts.deltaSourceId - : deltaOpts.deltaSource; - - const timeout = deltaOpts.deltaApplyTimeout; - - const logFn = (str: string) => - log.debug(`delta([${serviceName}] ${deltaOpts.deltaSource}): ${str}`); - - if (!_.includes([2, 3], deltaOpts.deltaVersion)) { - logFn( - `Unsupported delta version: ${deltaOpts.deltaVersion}. Falling back to regular pull`, - ); - return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); - } - - // We need to make sure that we're not trying to apply a - // v3 delta on top of a v2 delta, as this will cause the - // update to fail, and we must fall back to a standard - // image pull - if ( - deltaOpts.deltaVersion === 3 && - (await DockerUtils.isV2DeltaImage(this, deltaOpts.deltaSourceId)) - ) { - logFn( - `Cannot create a delta from V2 to V3, falling back to regular pull`, - ); - return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); - } - - // Since the supevisor never calls this function with a source anymore, - // this should never happen, but w ehandle it anyway - if (deltaOpts.deltaSource == null) { - logFn('Falling back to regular pull due to lack of a delta source'); - return this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); - } - - const docker = this; - logFn(`Starting delta to ${imgDest}`); - - const [dstInfo, srcInfo] = await Promise.all([ - this.getRegistryAndName(imgDest), - this.getRegistryAndName(deltaOpts.deltaSource), - ]); - - const token = await this.getAuthToken(srcInfo, dstInfo, deltaOpts); - - const opts: request.requestLib.CoreOptions = { - followRedirect: false, - timeout: deltaOpts.deltaRequestTimeout, - auth: { - bearer: token, - sendImmediately: true, - }, - }; - - const url = `${deltaOpts.deltaEndpoint}/api/v${deltaOpts.deltaVersion}/delta?src=${deltaOpts.deltaSource}&dest=${imgDest}`; - - const [res, data] = await (await request.getRequestInstance()).getAsync( - url, - opts, - ); - if (res.statusCode === 502 || res.statusCode === 504) { - throw new DeltaStillProcessingError(); - } - let id: string; - try { - switch (deltaOpts.deltaVersion) { - case 2: - if ( - !( - res.statusCode >= 300 && - res.statusCode < 400 && - res.headers['location'] != null - ) - ) { - throw new Error( - `Got ${res.statusCode} when requesting an image from delta server.`, - ); - } - const deltaUrl = res.headers['location']; - const deltaSrc = deltaSourceId; - const resumeOpts = { - timeout: deltaOpts.deltaRequestTimeout, - maxRetries: deltaOpts.deltaRetryCount, - retryInterval: deltaOpts.deltaRetryInterval, - }; - id = await DockerUtils.applyRsyncDelta( - deltaSrc, - deltaUrl, - timeout, - resumeOpts, - onProgress, - logFn, - ); - break; - case 3: - if (res.statusCode !== 200) { - throw new Error( - `Got ${res.statusCode} when requesting v3 delta from delta server.`, - ); - } - let name; - try { - name = JSON.parse(data).name; - } catch (e) { - throw new Error( - `Got an error when parsing delta server response for v3 delta: ${e}`, - ); - } - id = await DockerUtils.applyBalenaDelta( - docker, - name, - token, - onProgress, - logFn, - ); - break; - default: - throw new Error( - `Unsupported delta version: ${deltaOpts.deltaVersion}`, - ); - } - } catch (e) { - if (e instanceof OutOfSyncError) { - logFn('Falling back to regular pull due to delta out of sync error'); - return await this.fetchImageWithProgress( - imgDest, - deltaOpts, - onProgress, - ); - } else { - logFn(`Delta failed with ${e}`); - throw e; - } - } - - logFn(`Delta applied successfully`); - return id; + // We need to make sure that we're not trying to apply a + // v3 delta on top of a v2 delta, as this will cause the + // update to fail, and we must fall back to a standard + // image pull + if ( + deltaOpts.deltaVersion === 3 && + (await isV2DeltaImage(deltaOpts.deltaSourceId)) + ) { + logFn(`Cannot create a delta from V2 to V3, falling back to regular pull`); + return await fetchImageWithProgress(imgDest, deltaOpts, onProgress); } - public async fetchImageWithProgress( - image: string, - { uuid, currentApiKey }: FetchOptions, - onProgress: ProgressCallback, - ): Promise { - const { registry } = await this.getRegistryAndName(image); - - const dockerOpts = { - authconfig: { - username: `d_${uuid}`, - password: currentApiKey, - serverAddress: registry, - }, - }; - - await this.dockerProgress.pull(image, onProgress, dockerOpts); - return (await this.getImage(image).inspect()).Id; + // Since the supevisor never calls this function with a source anymore, + // this should never happen, but w ehandle it anyway + if (deltaOpts.deltaSource == null) { + logFn('Falling back to regular pull due to lack of a delta source'); + return fetchImageWithProgress(imgDest, deltaOpts, onProgress); } - public async getImageEnv(id: string): Promise { - const inspect = await this.getImage(id).inspect(); + logFn(`Starting delta to ${imgDest}`); - try { - return envArrayToObject(_.get(inspect, ['Config', 'Env'], [])); - } catch (e) { - log.error('Error getting env from image', e); - return {}; - } - } + const [dstInfo, srcInfo] = await Promise.all([ + dockerToolbelt.getRegistryAndName(imgDest), + dockerToolbelt.getRegistryAndName(deltaOpts.deltaSource), + ]); - public async getNetworkGateway(networkName: string): Promise { - if (networkName === 'host') { - return '127.0.0.1'; - } + const token = await getAuthToken(srcInfo, dstInfo, deltaOpts); - const network = await this.getNetwork(networkName).inspect(); - const config = _.get(network, ['IPAM', 'Config', '0']); - if (config != null) { - if (config.Gateway != null) { - return config.Gateway; - } - if (config.Subnet != null && _.endsWith(config.Subnet, '.0/16')) { - return config.Subnet.replace('.0/16', '.1'); - } - } - throw new InvalidNetGatewayError( - `Cannot determine network gateway for ${networkName}`, - ); - } - - private static applyRsyncDelta( - imgSrc: string, - deltaUrl: string, - applyTimeout: number, - opts: RsyncApplyOptions, - onProgress: ProgressCallback, - logFn: (str: string) => void, - ): Promise { - logFn('Applying rsync delta...'); - - return new Promise(async (resolve, reject) => { - const resumable = await request.getResumableRequest(); - const req = resumable(Object.assign({ url: deltaUrl }, opts)); - req - .on('progress', onProgress) - .on('retry', onProgress) - .on('error', reject) - .on('response', (res) => { - if (res.statusCode !== 200) { - reject( - new Error( - `Got ${res.statusCode} when requesting delta from storage.`, - ), - ); - } else if (parseInt(res.headers['content-length'] || '0', 10) === 0) { - reject(new Error('Invalid delta URL')); - } else { - const deltaStream = applyDelta(imgSrc, { - log: logFn, - timeout: applyTimeout, - }); - res - .pipe(deltaStream) - .on('id', (id) => resolve(`sha256:${id}`)) - .on('error', (err) => { - logFn(`Delta stream emitted error: ${err}`); - req.abort(); - reject(err); - }); - } - }); - }); - } - - private static async applyBalenaDelta( - docker: DockerUtils, - deltaImg: string, - token: string | null, - onProgress: ProgressCallback, - logFn: (str: string) => void, - ): Promise { - logFn('Applying balena delta...'); - - let auth: Dictionary | undefined; - if (token != null) { - logFn('Using registry auth token'); - auth = { - authconfig: { - registrytoken: token, - }, - }; - } - - await docker.dockerProgress.pull(deltaImg, onProgress, auth); - return (await docker.getImage(deltaImg).inspect()).Id; - } - - public static async isV2DeltaImage( - docker: DockerUtils, - imageName: string, - ): Promise { - const inspect = await docker.getImage(imageName).inspect(); - - // It's extremely unlikely that an image is valid if - // it's smaller than 40 bytes, but a v2 delta always is. - // For this reason, this is the method that we use to - // detect when an image is a v2 delta - return inspect.Size < 40 && inspect.VirtualSize < 40; - } - - private getAuthToken = memoizee( - async ( - srcInfo: ImageNameParts, - dstInfo: ImageNameParts, - deltaOpts: DeltaFetchOptions, - ): Promise => { - const tokenEndpoint = `${deltaOpts.apiEndpoint}/auth/v1/token`; - const tokenOpts: request.requestLib.CoreOptions = { - auth: { - user: `d_${deltaOpts.uuid}`, - pass: deltaOpts.currentApiKey, - sendImmediately: true, - }, - json: true, - }; - const tokenUrl = `${tokenEndpoint}?service=${dstInfo.registry}&scope=repository:${dstInfo.imageName}:pull&scope=repository:${srcInfo.imageName}:pull`; - - const tokenResponseBody = ( - await (await request.getRequestInstance()).getAsync(tokenUrl, tokenOpts) - )[1]; - const token = tokenResponseBody != null ? tokenResponseBody.token : null; - - if (token == null) { - throw new ImageAuthenticationError('Authentication error'); - } - - return token; + const opts: request.requestLib.CoreOptions = { + followRedirect: false, + timeout: deltaOpts.deltaRequestTimeout, + auth: { + bearer: token, + sendImmediately: true, }, - { maxAge: DELTA_TOKEN_TIMEOUT, promise: true }, + }; + + const url = `${deltaOpts.deltaEndpoint}/api/v${deltaOpts.deltaVersion}/delta?src=${deltaOpts.deltaSource}&dest=${imgDest}`; + + const [res, data] = await (await request.getRequestInstance()).getAsync( + url, + opts, + ); + if (res.statusCode === 502 || res.statusCode === 504) { + throw new DeltaStillProcessingError(); + } + let id: string; + try { + switch (deltaOpts.deltaVersion) { + case 2: + if ( + !( + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers['location'] != null + ) + ) { + throw new Error( + `Got ${res.statusCode} when requesting an image from delta server.`, + ); + } + const deltaUrl = res.headers['location']; + const deltaSrc = deltaSourceId; + const resumeOpts = { + timeout: deltaOpts.deltaRequestTimeout, + maxRetries: deltaOpts.deltaRetryCount, + retryInterval: deltaOpts.deltaRetryInterval, + }; + id = await applyRsyncDelta( + deltaSrc, + deltaUrl, + timeout, + resumeOpts, + onProgress, + logFn, + ); + break; + case 3: + if (res.statusCode !== 200) { + throw new Error( + `Got ${res.statusCode} when requesting v3 delta from delta server.`, + ); + } + let name; + try { + name = JSON.parse(data).name; + } catch (e) { + throw new Error( + `Got an error when parsing delta server response for v3 delta: ${e}`, + ); + } + id = await applyBalenaDelta(name, token, onProgress, logFn); + break; + default: + throw new Error(`Unsupported delta version: ${deltaOpts.deltaVersion}`); + } + } catch (e) { + if (e instanceof OutOfSyncError) { + logFn('Falling back to regular pull due to delta out of sync error'); + return await this.fetchImageWithProgress(imgDest, deltaOpts, onProgress); + } else { + logFn(`Delta failed with ${e}`); + throw e; + } + } + + logFn(`Delta applied successfully`); + return id; +} + +export async function fetchImageWithProgress( + image: string, + { uuid, currentApiKey }: FetchOptions, + onProgress: ProgressCallback, +): Promise { + const { registry } = await dockerToolbelt.getRegistryAndName(image); + + const dockerOpts = { + authconfig: { + username: `d_${uuid}`, + password: currentApiKey, + serverAddress: registry, + }, + }; + + await dockerProgress.pull(image, onProgress, dockerOpts); + return (await docker.getImage(image).inspect()).Id; +} + +export async function getImageEnv(id: string): Promise { + const inspect = await this.getImage(id).inspect(); + + try { + return envArrayToObject(_.get(inspect, ['Config', 'Env'], [])); + } catch (e) { + log.error('Error getting env from image', e); + return {}; + } +} + +export async function getNetworkGateway(networkName: string): Promise { + if (networkName === 'host') { + return '127.0.0.1'; + } + + const network = await this.getNetwork(networkName).inspect(); + const config = _.get(network, ['IPAM', 'Config', '0']); + if (config != null) { + if (config.Gateway != null) { + return config.Gateway; + } + if (config.Subnet != null && _.endsWith(config.Subnet, '.0/16')) { + return config.Subnet.replace('.0/16', '.1'); + } + } + throw new InvalidNetGatewayError( + `Cannot determine network gateway for ${networkName}`, ); } -export default DockerUtils; +function applyRsyncDelta( + imgSrc: string, + deltaUrl: string, + applyTimeout: number, + opts: RsyncApplyOptions, + onProgress: ProgressCallback, + logFn: (str: string) => void, +): Promise { + logFn('Applying rsync delta...'); + + return new Promise(async (resolve, reject) => { + const resumable = await request.getResumableRequest(); + const req = resumable(Object.assign({ url: deltaUrl }, opts)); + req + .on('progress', onProgress) + .on('retry', onProgress) + .on('error', reject) + .on('response', (res) => { + if (res.statusCode !== 200) { + reject( + new Error( + `Got ${res.statusCode} when requesting delta from storage.`, + ), + ); + } else if (parseInt(res.headers['content-length'] || '0', 10) === 0) { + reject(new Error('Invalid delta URL')); + } else { + const deltaStream = applyDelta(imgSrc, { + log: logFn, + timeout: applyTimeout, + }); + res + .pipe(deltaStream) + .on('id', (id) => resolve(`sha256:${id}`)) + .on('error', (err) => { + logFn(`Delta stream emitted error: ${err}`); + req.abort(); + reject(err); + }); + } + }); + }); +} + +async function applyBalenaDelta( + deltaImg: string, + token: string | null, + onProgress: ProgressCallback, + logFn: (str: string) => void, +): Promise { + logFn('Applying balena delta...'); + + let auth: Dictionary | undefined; + if (token != null) { + logFn('Using registry auth token'); + auth = { + authconfig: { + registrytoken: token, + }, + }; + } + + await dockerProgress.pull(deltaImg, onProgress, auth); + return (await docker.getImage(deltaImg).inspect()).Id; +} + +export async function isV2DeltaImage(imageName: string): Promise { + const inspect = await docker.getImage(imageName).inspect(); + + // It's extremely unlikely that an image is valid if + // it's smaller than 40 bytes, but a v2 delta always is. + // For this reason, this is the method that we use to + // detect when an image is a v2 delta + return inspect.Size < 40 && inspect.VirtualSize < 40; +} + +const getAuthToken = memoizee( + async ( + srcInfo: ImageNameParts, + dstInfo: ImageNameParts, + deltaOpts: DeltaFetchOptions, + ): Promise => { + const tokenEndpoint = `${deltaOpts.apiEndpoint}/auth/v1/token`; + const tokenOpts: request.requestLib.CoreOptions = { + auth: { + user: `d_${deltaOpts.uuid}`, + pass: deltaOpts.currentApiKey, + sendImmediately: true, + }, + json: true, + }; + const tokenUrl = `${tokenEndpoint}?service=${dstInfo.registry}&scope=repository:${dstInfo.imageName}:pull&scope=repository:${srcInfo.imageName}:pull`; + + const tokenResponseBody = ( + await (await request.getRequestInstance()).getAsync(tokenUrl, tokenOpts) + )[1]; + const token = tokenResponseBody != null ? tokenResponseBody.token : null; + + if (token == null) { + throw new ImageAuthenticationError('Authentication error'); + } + + return token; + }, + { maxAge: DELTA_TOKEN_TIMEOUT, promise: true }, +); diff --git a/src/lib/migration.ts b/src/lib/migration.ts index 5c45608b..6969152c 100644 --- a/src/lib/migration.ts +++ b/src/lib/migration.ts @@ -15,6 +15,7 @@ import * as db from '../db'; import DeviceState from '../device-state'; import * as constants from '../lib/constants'; import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors'; +import { docker } from '../lib/docker-utils'; import { pathExistsOnHost } from '../lib/fs-utils'; import { log } from '../lib/supervisor-console'; 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}`, ); - const imageFromDocker = await application.docker + const imageFromDocker = await docker .getImage(service.image) .inspect() .catch((error) => { diff --git a/src/local-mode.ts b/src/local-mode.ts index f77d532f..611006e1 100644 --- a/src/local-mode.ts +++ b/src/local-mode.ts @@ -1,10 +1,10 @@ import * as Bluebird from 'bluebird'; -import * as Docker from 'dockerode'; import * as _ from 'lodash'; import * as config from './config'; import * as db from './db'; import * as constants from './lib/constants'; +import { docker } from './lib/docker-utils'; import { SupervisorContainerNotFoundError } from './lib/errors'; import log from './lib/supervisor-console'; import { Logger } from './logger'; @@ -71,7 +71,6 @@ const SUPERVISOR_CONTAINER_NAME_FALLBACK = 'resin_supervisor'; */ export class LocalModeManager { public constructor( - public docker: Docker, public logger: Logger, private containerId: string | undefined = constants.containerId, ) {} @@ -121,16 +120,14 @@ export class LocalModeManager { // Query the engine to get currently running containers and installed images. public async collectEngineSnapshot(): Promise { - const containersPromise = this.docker + const containersPromise = docker .listContainers() .then((resp) => _.map(resp, 'Id')); - const imagesPromise = this.docker - .listImages() - .then((resp) => _.map(resp, 'Id')); - const volumesPromise = this.docker + const imagesPromise = docker.listImages().then((resp) => _.map(resp, 'Id')); + const volumesPromise = docker .listVolumes() .then((resp) => _.map(resp.Volumes, 'Name')); - const networksPromise = this.docker + const networksPromise = docker .listNetworks() .then((resp) => _.map(resp, 'Id')); @@ -149,7 +146,7 @@ export class LocalModeManager { private async collectContainerResources( nameOrId: string, ): Promise { - const inspectInfo = await this.docker.getContainer(nameOrId).inspect(); + const inspectInfo = await docker.getContainer(nameOrId).inspect(); return new EngineSnapshot( [inspectInfo.Id], [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. await Bluebird.map(objects.containers, (cId) => { - return this.docker + return docker .getContainer(cId) .remove({ force: true }) .catch((e) => log.error(`Unable to delete container ${cId}`, e)); }); await Bluebird.map(objects.images, (iId) => { - return this.docker + return docker .getImage(iId) .remove({ force: true }) .catch((e) => log.error(`Unable to delete image ${iId}`, e)); }); await Bluebird.map(objects.networks, (nId) => { - return this.docker + return docker .getNetwork(nId) .remove() .catch((e) => log.error(`Unable to delete network ${nId}`, e)); }); await Bluebird.map(objects.volumes, (vId) => { - return this.docker + return docker .getVolume(vId) .remove() .catch((e) => log.error(`Unable to delete volume ${vId}`, e)); diff --git a/src/logger.ts b/src/logger.ts index 0c45d03c..3a67c069 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -4,7 +4,6 @@ import * as _ from 'lodash'; import * as config from './config'; import * as db from './db'; import { EventTracker } from './event-tracker'; -import Docker from './lib/docker-utils'; import { LogType } from './lib/log-types'; import { writeLock } from './lib/update-lock'; import { @@ -159,7 +158,6 @@ export class Logger { } public attach( - docker: Docker, containerId: string, serviceInfo: { serviceId: number; imageId: number }, ): Bluebird { @@ -170,7 +168,7 @@ export class Logger { } return Bluebird.using(this.lock(containerId), async () => { - const logs = new ContainerLogs(containerId, docker); + const logs = new ContainerLogs(containerId); this.containerLogs[containerId] = logs; logs.on('error', (err) => { log.error('Container log retrieval error', err); diff --git a/src/logging/container.ts b/src/logging/container.ts index 77d81eb7..f3cdc012 100644 --- a/src/logging/container.ts +++ b/src/logging/container.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import * as Stream from 'stream'; import StrictEventEmitter from 'strict-event-emitter-types'; -import Docker from '../lib/docker-utils'; +import { docker } from '../lib/docker-utils'; export interface ContainerLog { message: string; @@ -21,7 +21,7 @@ interface LogsEvents { type LogsEventEmitter = StrictEventEmitter; export class ContainerLogs extends (EventEmitter as new () => LogsEventEmitter) { - public constructor(public containerId: string, private docker: Docker) { + public constructor(public containerId: string) { super(); } @@ -34,7 +34,7 @@ export class ContainerLogs extends (EventEmitter as new () => LogsEventEmitter) const stdoutLogOpts = { stdout: true, stderr: 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 stderrStream = await container.logs(stderrLogOpts); diff --git a/src/proxyvisor.js b/src/proxyvisor.js index a255a6d9..a27c0e43 100644 --- a/src/proxyvisor.js +++ b/src/proxyvisor.js @@ -17,6 +17,7 @@ import * as url from 'url'; import { log } from './lib/supervisor-console'; import * as db from './db'; import * as config from './config'; +import * as dockerUtils from './lib/docker-utils'; const mkdirpAsync = Promise.promisify(mkdirp); @@ -344,7 +345,7 @@ const createProxyvisorRouter = function (proxyvisor) { }; export class Proxyvisor { - constructor({ logger, docker, images, applications }) { + constructor({ logger, images, applications }) { this.bindToAPI = this.bindToAPI.bind(this); this.executeStepAction = this.executeStepAction.bind(this); this.getCurrentStates = this.getCurrentStates.bind(this); @@ -361,7 +362,6 @@ export class Proxyvisor { this.sendDeleteHook = this.sendDeleteHook.bind(this); this.sendUpdates = this.sendUpdates.bind(this); this.logger = logger; - this.docker = docker; this.images = images; this.applications = applications; this.acknowledgedState = {}; @@ -904,7 +904,7 @@ export class Proxyvisor { }) .then((parentApp) => { return Promise.map(parentApp?.services ?? [], (service) => { - return this.docker.getImageEnv(service.image); + return dockerUtils.getImageEnv(service.image); }).then(function (imageEnvs) { const imageHookAddresses = _.map( imageEnvs, diff --git a/test/05-device-state.spec.ts b/test/05-device-state.spec.ts index 6931b9e3..d94377cc 100644 --- a/test/05-device-state.spec.ts +++ b/test/05-device-state.spec.ts @@ -6,6 +6,7 @@ import { SinonSpy, SinonStub, spy, stub } from 'sinon'; import chai = require('./lib/chai-config'); import prepare = require('./lib/prepare'); import Log from '../src/lib/supervisor-console'; +import * as dockerUtils from '../src/lib/docker-utils'; import * as config from '../src/config'; import { RPiConfigBackend } from '../src/config/backend'; import DeviceState from '../src/device-state'; @@ -235,7 +236,7 @@ describe('deviceState', () => { apiBinder: null as any, }); - stub(deviceState.applications.docker, 'getNetworkGateway').returns( + stub(dockerUtils, 'getNetworkGateway').returns( Promise.resolve('172.17.0.1'), ); @@ -250,8 +251,7 @@ describe('deviceState', () => { after(() => { (Service as any).extendEnvVars.restore(); - (deviceState.applications.docker - .getNetworkGateway as sinon.SinonStub).restore(); + (dockerUtils.getNetworkGateway as sinon.SinonStub).restore(); (deviceState.applications.images .inspectByName as sinon.SinonStub).restore(); }); diff --git a/test/14-application-manager.spec.ts b/test/14-application-manager.spec.ts index a0c87c41..6bb63c5d 100644 --- a/test/14-application-manager.spec.ts +++ b/test/14-application-manager.spec.ts @@ -8,6 +8,7 @@ import Service from '../src/compose/service'; import Volume from '../src/compose/volume'; import DeviceState from '../src/device-state'; import EventTracker from '../src/event-tracker'; +import * as dockerUtils from '../src/lib/docker-utils'; import chai = require('./lib/chai-config'); 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'), ); - stub(this.applications.docker, 'listContainers').returns( + stub(dockerUtils.docker, 'listContainers').returns( Bluebird.Promise.resolve([]), ); - stub(this.applications.docker, 'listImages').returns( + stub(dockerUtils.docker, 'listImages').returns( Bluebird.Promise.resolve([]), ); stub(Service as any, 'extendEnvVars').callsFake(function (env) { @@ -174,7 +175,6 @@ describe('ApplicationManager', function () { appCloned.networks, (config, name) => { return Network.fromComposeObject(name, app.appId, config, { - docker: this.applications.docker, logger: this.logger, }); }, @@ -235,8 +235,12 @@ describe('ApplicationManager', function () { after(function () { this.applications.images.inspectByName.restore(); - this.applications.docker.getNetworkGateway.restore(); - this.applications.docker.listContainers.restore(); + // @ts-expect-error restore on non-stubbed type + 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(); }); diff --git a/test/18-startup.ts b/test/18-startup.ts index 1e623750..7a77783b 100644 --- a/test/18-startup.ts +++ b/test/18-startup.ts @@ -4,7 +4,7 @@ import APIBinder from '../src/api-binder'; import { ApplicationManager } from '../src/application-manager'; import DeviceState from '../src/device-state'; 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 { expect } from './lib/chai-config'; @@ -30,9 +30,7 @@ describe('Startup', () => { deviceStateStub = stub(DeviceState.prototype as any, 'applyTarget').returns( Promise.resolve(), ); - dockerStub = stub(Docker.prototype, 'listContainers').returns( - Promise.resolve([]), - ); + dockerStub = stub(docker, 'listContainers').returns(Promise.resolve([])); }); after(() => { diff --git a/test/20-compose-volume.ts b/test/20-compose-volume.ts index 9e1baed8..eeed3b93 100644 --- a/test/20-compose-volume.ts +++ b/test/20-compose-volume.ts @@ -1,5 +1,7 @@ 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 logTypes = require('../src/lib/log-types'); @@ -8,13 +10,18 @@ const fakeLogger = { logSystemMessage: stub(), logSystemEvent: stub(), }; -const fakeDocker = { - createVolume: stub(), -}; -const opts: any = { logger: fakeLogger, docker: fakeDocker }; +const opts: any = { logger: fakeLogger }; describe('Compose volumes', () => { + let createVolumeStub: SinonStub; + before(() => { + createVolumeStub = stub(docker, 'createVolume'); + }); + after(() => { + createVolumeStub.restore(); + }); + describe('Parsing volumes', () => { it('should correctly parse docker volumes', () => { const volume = Volume.fromDockerVolume(opts, { @@ -122,7 +129,7 @@ describe('Compose volumes', () => { describe('Generating docker options', () => { afterEach(() => { - fakeDocker.createVolume.reset(); + createVolumeStub.reset(); fakeLogger.logSystemEvent.reset(); fakeLogger.logSystemMessage.reset(); }); @@ -143,7 +150,7 @@ describe('Compose volumes', () => { await volume.create(); expect( - fakeDocker.createVolume.calledWith({ + createVolumeStub.calledWith({ Labels: { 'my-label': 'test-label', 'io.balena.supervised': 'true', diff --git a/test/23-local-mode.ts b/test/23-local-mode.ts index 08b2a740..e50cf7ac 100644 --- a/test/23-local-mode.ts +++ b/test/23-local-mode.ts @@ -4,6 +4,7 @@ import * as Docker from 'dockerode'; import * as sinon from 'sinon'; import * as db from '../src/db'; +import { docker } from '../src/lib/docker-utils'; import LocalModeManager, { EngineSnapshot, EngineSnapshotRecord, @@ -13,7 +14,7 @@ import ShortStackError from './lib/errors'; describe('LocalModeManager', () => { let localMode: LocalModeManager; - let dockerStub: sinon.SinonStubbedInstance; + let dockerStub: sinon.SinonStubbedInstance; const supervisorContainerId = 'super-container-1'; @@ -32,14 +33,14 @@ describe('LocalModeManager', () => { before(async () => { await db.initialized; - dockerStub = sinon.createStubInstance(Docker); + dockerStub = sinon.stub(docker); const loggerStub = (sinon.createStubInstance(Logger) as unknown) as Logger; - localMode = new LocalModeManager( - (dockerStub as unknown) as Docker, - loggerStub, - supervisorContainerId, - ); + localMode = new LocalModeManager(loggerStub, supervisorContainerId); + }); + + after(async () => { + sinon.restore(); }); describe('EngineSnapshot', () => { @@ -427,8 +428,4 @@ describe('LocalModeManager', () => { }); }); }); - - after(async () => { - sinon.restore(); - }); }); diff --git a/test/25-deltas.spec.ts b/test/25-deltas.spec.ts index 1f239a2a..03044301 100644 --- a/test/25-deltas.spec.ts +++ b/test/25-deltas.spec.ts @@ -1,13 +1,11 @@ import { expect } from 'chai'; import { stub } from 'sinon'; -import DockerUtils from '../src/lib/docker-utils'; - -const dockerUtils = new DockerUtils({}); +import * as dockerUtils from '../src/lib/docker-utils'; describe('Deltas', () => { it('should correctly detect a V2 delta', async () => { - const imageStub = stub(dockerUtils, 'getImage').returns({ + const imageStub = stub(dockerUtils.docker, 'getImage').returns({ inspect: () => { return Promise.resolve({ Id: @@ -99,7 +97,7 @@ describe('Deltas', () => { }, } 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); imageStub.restore(); });