mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-05 10:32:56 +00:00
Merge pull request #1351 from balena-io/singleton-docker
Make docker module a singleton
This commit is contained in:
commit
96e55585ea
2
src/application-manager.d.ts
vendored
2
src/application-manager.d.ts
vendored
@ -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;
|
||||||
|
@ -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,11 +252,9 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
return this.docker
|
return docker.listContainers({ all: true }).then((containers) => {
|
||||||
.listContainers({ all: true })
|
return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||||
.then((containers) => {
|
});
|
||||||
return this.logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
// Rather than relying on removing out of date database entries when we're no
|
// 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
|
// 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) {
|
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({
|
||||||
|
@ -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
|
||||||
|
@ -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'],
|
||||||
},
|
},
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}`,
|
||||||
|
@ -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'] },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@ -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) {
|
||||||
|
@ -42,338 +42,316 @@ 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);
|
image: string,
|
||||||
this.dockerProgress = new DockerProgress({ dockerToolbelt: this });
|
): 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(
|
return { repo: repoName, tag: tagName };
|
||||||
image: string,
|
}
|
||||||
): Promise<{ repo: string; tag: string }> {
|
|
||||||
const { registry, imageName, tagName } = await this.getRegistryAndName(
|
export async function fetchDeltaWithProgress(
|
||||||
image,
|
imgDest: string,
|
||||||
|
deltaOpts: DeltaFetchOptions,
|
||||||
|
onProgress: ProgressCallback,
|
||||||
|
serviceName: string,
|
||||||
|
): Promise<string> {
|
||||||
|
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 fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
||||||
let repoName = imageName;
|
|
||||||
|
|
||||||
if (registry != null) {
|
|
||||||
repoName = `${registry}/${imageName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { repo: repoName, tag: tagName };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchDeltaWithProgress(
|
// We need to make sure that we're not trying to apply a
|
||||||
imgDest: string,
|
// v3 delta on top of a v2 delta, as this will cause the
|
||||||
deltaOpts: DeltaFetchOptions,
|
// update to fail, and we must fall back to a standard
|
||||||
onProgress: ProgressCallback,
|
// image pull
|
||||||
serviceName: string,
|
if (
|
||||||
): Promise<string> {
|
deltaOpts.deltaVersion === 3 &&
|
||||||
const deltaSourceId =
|
(await isV2DeltaImage(deltaOpts.deltaSourceId))
|
||||||
deltaOpts.deltaSourceId != null
|
) {
|
||||||
? deltaOpts.deltaSourceId
|
logFn(`Cannot create a delta from V2 to V3, falling back to regular pull`);
|
||||||
: deltaOpts.deltaSource;
|
return await fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchImageWithProgress(
|
// Since the supevisor never calls this function with a source anymore,
|
||||||
image: string,
|
// this should never happen, but w ehandle it anyway
|
||||||
{ uuid, currentApiKey }: FetchOptions,
|
if (deltaOpts.deltaSource == null) {
|
||||||
onProgress: ProgressCallback,
|
logFn('Falling back to regular pull due to lack of a delta source');
|
||||||
): Promise<string> {
|
return fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImageEnv(id: string): Promise<EnvVarObject> {
|
logFn(`Starting delta to ${imgDest}`);
|
||||||
const inspect = await this.getImage(id).inspect();
|
|
||||||
|
|
||||||
try {
|
const [dstInfo, srcInfo] = await Promise.all([
|
||||||
return envArrayToObject(_.get(inspect, ['Config', 'Env'], []));
|
dockerToolbelt.getRegistryAndName(imgDest),
|
||||||
} catch (e) {
|
dockerToolbelt.getRegistryAndName(deltaOpts.deltaSource),
|
||||||
log.error('Error getting env from image', e);
|
]);
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getNetworkGateway(networkName: string): Promise<string> {
|
const token = await getAuthToken(srcInfo, dstInfo, deltaOpts);
|
||||||
if (networkName === 'host') {
|
|
||||||
return '127.0.0.1';
|
|
||||||
}
|
|
||||||
|
|
||||||
const network = await this.getNetwork(networkName).inspect();
|
const opts: request.requestLib.CoreOptions = {
|
||||||
const config = _.get(network, ['IPAM', 'Config', '0']);
|
followRedirect: false,
|
||||||
if (config != null) {
|
timeout: deltaOpts.deltaRequestTimeout,
|
||||||
if (config.Gateway != null) {
|
auth: {
|
||||||
return config.Gateway;
|
bearer: token,
|
||||||
}
|
sendImmediately: true,
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
logFn('Applying balena delta...');
|
|
||||||
|
|
||||||
let auth: Dictionary<unknown> | 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<boolean> {
|
|
||||||
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<string> => {
|
|
||||||
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 },
|
};
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
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<EnvVarObject> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
logFn('Applying balena delta...');
|
||||||
|
|
||||||
|
let auth: Dictionary<unknown> | 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<boolean> {
|
||||||
|
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<string> => {
|
||||||
|
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 },
|
||||||
|
);
|
||||||
|
@ -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) => {
|
||||||
|
@ -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));
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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',
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user