Merge pull request #1676 from balena-os/test-improvements

Bump dockerode types to 2.5.34
This commit is contained in:
bulldozer-balena[bot] 2021-04-27 18:20:51 +00:00 committed by GitHub
commit 404f5c77d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 432 additions and 346 deletions

6
package-lock.json generated
View File

@ -471,9 +471,9 @@
"dev": true
},
"@types/dockerode": {
"version": "2.5.28",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.28.tgz",
"integrity": "sha512-YHs025G2P+h+CDlmub33SY/CvGWCHpHATbgq73wykyvnZZjL0Iq+w+Vv214w3cac5McimFhsVecDJBNgPuMo4Q==",
"version": "2.5.34",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.34.tgz",
"integrity": "sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA==",
"dev": true,
"requires": {
"@types/node": "*"

View File

@ -49,7 +49,7 @@
"@types/common-tags": "^1.8.0",
"@types/copy-webpack-plugin": "^6.0.0",
"@types/dbus": "^1.0.0",
"@types/dockerode": "^2.5.28",
"@types/dockerode": "^2.5.34",
"@types/event-stream": "^3.3.34",
"@types/express": "^4.17.3",
"@types/lockfile": "^1.0.1",

View File

@ -1,20 +1,13 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as dockerode from 'dockerode';
import { docker } from '../lib/docker-utils';
import { InvalidAppIdError } from '../lib/errors';
import logTypes = require('../lib/log-types');
import { checkInt } from '../lib/validation';
import * as logger from '../logger';
import * as ComposeUtils from './utils';
import {
ComposeNetworkConfig,
DockerIPAMConfig,
DockerNetworkConfig,
NetworkConfig,
NetworkInspect,
} from './types/network';
import { ComposeNetworkConfig, NetworkConfig } from './types/network';
import {
InvalidNetworkConfigurationError,
@ -28,50 +21,42 @@ export class Network {
private constructor() {}
public static fromDockerNetwork(network: NetworkInspect): Network {
public static fromDockerNetwork(
network: dockerode.NetworkInspectInfo,
): Network {
const ret = new Network();
const match = network.Name.match(/^([0-9]+)_(.+)$/);
if (match == null) {
throw new InvalidNetworkNameError(network.Name);
}
const appId = checkInt(match[1]) || null;
if (!appId) {
throw new InvalidAppIdError(match[1]);
}
// If the regex match succeeds `match[1]` should be a number
const appId = parseInt(match[1], 10);
ret.appId = appId;
ret.name = match[2];
const config = network.IPAM?.Config || [];
ret.config = {
driver: network.Driver,
ipam: {
driver: network.IPAM.Driver,
config: _.map(network.IPAM.Config, (conf) => {
const newConf: NetworkConfig['ipam']['config'][0] = {};
if (conf.Subnet != null) {
newConf.subnet = conf.Subnet;
}
if (conf.Gateway != null) {
newConf.gateway = conf.Gateway;
}
if (conf.IPRange != null) {
newConf.ipRange = conf.IPRange;
}
if (conf.AuxAddress != null) {
newConf.auxAddress = conf.AuxAddress;
}
return newConf;
}),
options: network.IPAM.Options == null ? {} : network.IPAM.Options,
driver: network.IPAM?.Driver ?? 'default',
config: config.map((conf) => ({
...(conf.Subnet && { subnet: conf.Subnet }),
...(conf.Gateway && { gateway: conf.Gateway }),
...(conf.IPRange && { ipRange: conf.IPRange }),
...(conf.AuxAddress && { auxAddress: conf.AuxAddress }),
})),
options: network.IPAM?.Options ?? {},
},
enableIPv6: network.EnableIPv6,
internal: network.Internal,
labels: _.omit(ComposeUtils.normalizeLabels(network.Labels), [
labels: _.omit(ComposeUtils.normalizeLabels(network.Labels ?? {}), [
'io.balena.supervised',
]),
options: network.Options,
options: network.Options ?? {},
};
return ret;
@ -90,19 +75,26 @@ export class Network {
Network.validateComposeConfig(network);
const ipam: Partial<ComposeNetworkConfig['ipam']> = network.ipam || {};
if (ipam.driver == null) {
ipam.driver = 'default';
}
if (ipam.config == null) {
ipam.config = [];
}
if (ipam.options == null) {
ipam.options = {};
}
const ipam = network.ipam ?? {};
const driver = ipam.driver ?? 'default';
const config = ipam.config ?? [];
const options = ipam.options ?? {};
net.config = {
driver: network.driver || 'bridge',
ipam: ipam as ComposeNetworkConfig['ipam'],
ipam: {
driver,
config: config.map((conf) => ({
...(conf.subnet && { subnet: conf.subnet }),
...(conf.gateway && { gateway: conf.gateway }),
...(conf.ip_range && { ipRange: conf.ip_range }),
// TODO: compose defines aux_addresses as a dict but dockerode and the
// engine accepts a single AuxAddress. What happens when multiple addresses
// are given?
...(conf.aux_addresses && { auxAddress: conf.aux_addresses }),
})) as ComposeNetworkConfig['ipam']['config'],
options,
},
enableIPv6: network.enable_ipv6 || false,
internal: network.internal || false,
labels: network.labels || {},
@ -130,31 +122,23 @@ export class Network {
network: { name: this.name },
});
return await docker.createNetwork(this.toDockerConfig());
await docker.createNetwork(this.toDockerConfig());
}
public toDockerConfig(): DockerNetworkConfig {
public toDockerConfig(): dockerode.NetworkCreateOptions {
return {
Name: Network.generateDockerName(this.appId, this.name),
Driver: this.config.driver,
CheckDuplicate: true,
IPAM: {
Driver: this.config.ipam.driver,
Config: _.map(this.config.ipam.config, (conf) => {
const ipamConf: DockerIPAMConfig = {};
if (conf.subnet != null) {
ipamConf.Subnet = conf.subnet;
}
if (conf.gateway != null) {
ipamConf.Gateway = conf.gateway;
}
if (conf.auxAddress != null) {
ipamConf.AuxAddress = conf.auxAddress;
}
if (conf.ipRange != null) {
ipamConf.IPRange = conf.ipRange;
}
return ipamConf;
Config: this.config.ipam.config.map((conf) => {
return {
...(conf.subnet && { Subnet: conf.subnet }),
...(conf.gateway && { Gateway: conf.gateway }),
...(conf.auxAddress && { AuxAddress: conf.auxAddress }),
...(conf.ipRange && { IPRange: conf.ipRange }),
};
}),
Options: this.config.ipam.options,
},

View File

@ -1,39 +1,3 @@
// It appears the dockerode typings are incomplete,
// extend here for now.
// TODO: Upstream these to definitelytyped
export interface NetworkInspect {
Name: string;
Id: string;
Created: string;
Scope: string;
Driver: string;
EnableIPv6: boolean;
IPAM: {
Driver: string;
Options: null | { [optName: string]: string };
Config: Array<{
Subnet: string;
Gateway: string;
IPRange?: string;
AuxAddress?: string;
}>;
};
Internal: boolean;
Attachable: boolean;
Ingress: boolean;
Containers: {
[containerId: string]: {
Name: string;
EndpointID: string;
MacAddress: string;
IPv4Address: string;
IPv6Address: string;
};
};
Options: { [optName: string]: string };
Labels: { [labelName: string]: string };
}
export interface ComposeNetworkConfig {
driver: string;
driver_opts: Dictionary<string>;
@ -70,27 +34,3 @@ export interface NetworkConfig {
labels: { [labelName: string]: string };
options: { [optName: string]: string };
}
export interface DockerIPAMConfig {
Subnet?: string;
IPRange?: string;
Gateway?: string;
AuxAddress?: string;
}
export interface DockerNetworkConfig {
Name: string;
Driver?: string;
CheckDuplicate: boolean;
IPAM?: {
Driver?: string;
Config?: DockerIPAMConfig[];
Options?: Dictionary<string>;
};
Internal?: boolean;
Attachable?: boolean;
Ingress?: boolean;
Options?: Dictionary<string>;
Labels?: Dictionary<string>;
EnableIPv6?: boolean;
}

View File

@ -1,125 +0,0 @@
import { expect } from './lib/chai-config';
import { Network } from '../src/compose/network';
describe('compose/network', function () {
describe('compose config -> internal config', function () {
it('should convert a compose configuration to an internal representation', function () {
const network = Network.fromComposeObject(
'test',
123,
{
driver: 'bridge',
ipam: {
driver: 'default',
config: [
{
subnet: '172.25.0.0/25',
gateway: '172.25.0.1',
},
],
},
},
// @ts-ignore ignore passing nulls instead of actual objects
{ logger: null, docker: null },
);
expect(network.config).to.deep.equal({
driver: 'bridge',
ipam: {
driver: 'default',
config: [
{
subnet: '172.25.0.0/25',
gateway: '172.25.0.1',
},
],
options: {},
},
enableIPv6: false,
internal: false,
labels: {},
options: {},
});
});
it('should handle an incomplete ipam configuration', function () {
const network = Network.fromComposeObject(
'test',
123,
{
ipam: {
config: [
{
subnet: '172.25.0.0/25',
gateway: '172.25.0.1',
},
],
},
},
// @ts-ignore ignore passing nulls instead of actual objects
{ logger: null, docker: null },
);
expect(network.config).to.deep.equal({
driver: 'bridge',
enableIPv6: false,
internal: false,
labels: {},
options: {},
ipam: {
driver: 'default',
options: {},
config: [
{
subnet: '172.25.0.0/25',
gateway: '172.25.0.1',
},
],
},
});
});
});
describe('internal config -> docker config', () =>
it('should convert an internal representation to a docker representation', function () {
const network = Network.fromComposeObject(
'test',
123,
{
driver: 'bridge',
ipam: {
driver: 'default',
config: [
{
subnet: '172.25.0.0/25',
gateway: '172.25.0.1',
},
],
},
},
// @ts-ignore ignore passing nulls instead of actual objects
{ logger: null, docker: null },
);
expect(network.toDockerConfig()).to.deep.equal({
Name: '123_test',
Driver: 'bridge',
CheckDuplicate: true,
IPAM: {
Driver: 'default',
Config: [
{
Subnet: '172.25.0.0/25',
Gateway: '172.25.0.1',
},
],
Options: {},
},
EnableIPv6: false,
Internal: false,
Labels: {
'io.balena.supervised': 'true',
},
});
}));
});

View File

@ -73,6 +73,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
afterEach(() => {
// Clear Dockerode actions recorded for each test
mockedDockerode.resetHistory();
appMock.unmockAll();
});
before(async () => {
@ -120,7 +121,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
healthCheckStubs.forEach((hc) => hc.restore());
// Remove any test data generated
await mockedAPI.cleanUp();
appMock.unmockAll();
targetStateCacheMock.restore();
loggerStub.restore();
});
@ -213,7 +213,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('GET /v1/apps/:appId', () => {
it('does not return information for an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
@ -236,32 +235,39 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
});
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
// Make request
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
// Make request
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
},
);
});
});
describe('POST /v1/apps/:appId/stop', () => {
it('does not allow stopping an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
.statusCode,
);
await mockedDockerode.testWithData({ containers, images }, async () => {
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
.statusCode,
);
});
});
it('stops a SPECIFIC application and returns a containerId', async () => {
@ -298,11 +304,13 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('POST /v1/apps/:appId/start', () => {
it('does not allow starting an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request
await request
.post('/v1/apps/2/start')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(400);
await mockedDockerode.testWithData({ containers, images }, async () => {
await request
.post('/v1/apps/2/start')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(400);
});
});
it('starts a SPECIFIC application and returns a containerId', async () => {
@ -387,14 +395,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
},
);
});
it('should return 423 and reject the reboot if no locks are set', async () => {
@ -417,15 +430,20 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(rebootMock).to.not.have.been.called;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(rebootMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -450,16 +468,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/reboot')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -488,14 +511,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
},
);
shutdownMock.resetHistory();
});
@ -520,15 +548,20 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(shutdownMock).to.not.have.been.called;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(shutdownMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -553,16 +586,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/shutdown')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore();
});

View File

@ -361,13 +361,15 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
});
it('should return 404 for an unknown service', async () => {
await request
.post(`/v2/applications/1658654/start-service?apikey=${appScopedKey}`)
.send({ serviceName: 'unknown' })
.set('Content-type', 'application/json')
.expect(404);
await mockedDockerode.testWithData({}, async () => {
await request
.post(`/v2/applications/1658654/start-service?apikey=${appScopedKey}`)
.send({ serviceName: 'unknown' })
.set('Content-type', 'application/json')
.expect(404);
expect(applicationManagerSpy).to.not.have.been.called;
expect(applicationManagerSpy).to.not.have.been.called;
});
});
it('should ignore locks and return 200', async () => {
@ -465,12 +467,16 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
});
it('should return 404 for an unknown service', async () => {
await request
.post(`/v2/applications/1658654/restart-service?apikey=${appScopedKey}`)
.send({ serviceName: 'unknown' })
.set('Content-type', 'application/json')
.expect(404);
expect(applicationManagerSpy).to.not.have.been.called;
await mockedDockerode.testWithData({}, async () => {
await request
.post(
`/v2/applications/1658654/restart-service?apikey=${appScopedKey}`,
)
.send({ serviceName: 'unknown' })
.set('Content-type', 'application/json')
.expect(404);
expect(applicationManagerSpy).to.not.have.been.called;
});
});
it('should return 423 for a service with update locks', async () => {

View File

@ -2,9 +2,10 @@ import ChaiConfig = require('../lib/chai-config');
const { expect } = ChaiConfig;
import { Network } from '../../src/compose/network';
import { NetworkInspectInfo } from 'dockerode';
describe('Network', () => {
describe('fromComposeObject', () => {
describe('compose/network', () => {
describe('creating a network from a compose object', () => {
it('creates a default network configuration if no config is given', () => {
const network = Network.fromComposeObject('default', 12345, {});
@ -54,6 +55,7 @@ describe('Network', () => {
{
subnet: '172.20.0.0/16',
ip_range: '172.20.10.0/24',
aux_addresses: { host0: '172.20.10.15', host1: '172.20.10.16' },
gateway: '172.20.0.1',
},
],
@ -67,8 +69,9 @@ describe('Network', () => {
config: [
{
subnet: '172.20.0.0/16',
ip_range: '172.20.10.0/24',
ipRange: '172.20.10.0/24',
gateway: '172.20.0.1',
auxAddress: { host0: '172.20.10.15', host1: '172.20.10.16' },
},
],
options: {},
@ -109,4 +112,221 @@ describe('Network', () => {
);
});
});
describe('creating a network from docker engine state', () => {
it('rejects networks without the proper name format', () => {
expect(() =>
Network.fromDockerNetwork({
Id: 'deadbeef',
Name: 'abcd',
} as NetworkInspectInfo),
).to.throw();
expect(() =>
Network.fromDockerNetwork({
Id: 'deadbeef',
Name: 'abcd_1234',
} as NetworkInspectInfo),
).to.throw();
expect(() =>
Network.fromDockerNetwork({
Id: 'deadbeef',
Name: 'abcd_abcd',
} as NetworkInspectInfo),
).to.throw();
expect(() =>
Network.fromDockerNetwork({
Id: 'deadbeef',
Name: '1234',
} as NetworkInspectInfo),
).to.throw();
});
it('creates a network object from a docker network configuration', () => {
const network = Network.fromDockerNetwork({
Id: 'deadbeef',
Name: '1234_default',
Driver: 'bridge',
EnableIPv6: true,
IPAM: {
Driver: 'default',
Options: {},
Config: [
{
Subnet: '172.18.0.0/16',
Gateway: '172.18.0.1',
},
],
} as NetworkInspectInfo['IPAM'],
Internal: true,
Containers: {},
Options: {
'com.docker.some-option': 'abcd',
} as NetworkInspectInfo['Options'],
Labels: {
'io.balena.features.something': '123',
} as NetworkInspectInfo['Labels'],
} as NetworkInspectInfo);
expect(network.appId).to.equal(1234);
expect(network.name).to.equal('default');
expect(network.config.enableIPv6).to.equal(true);
expect(network.config.ipam.driver).to.equal('default');
expect(network.config.ipam.options).to.deep.equal({});
expect(network.config.ipam.config).to.deep.equal([
{
subnet: '172.18.0.0/16',
gateway: '172.18.0.1',
},
]);
expect(network.config.internal).to.equal(true);
expect(network.config.options).to.deep.equal({
'com.docker.some-option': 'abcd',
});
expect(network.config.labels).to.deep.equal({
'io.balena.features.something': '123',
});
});
it('normalizes legacy label names and excludes supervised label', () => {
const network = Network.fromDockerNetwork({
Id: 'deadbeef',
Name: '1234_default',
IPAM: {
Driver: 'default',
Options: {},
Config: [],
} as NetworkInspectInfo['IPAM'],
Labels: {
'io.resin.features.something': '123',
'io.balena.features.dummy': 'abc',
'io.balena.supervised': 'true',
} as NetworkInspectInfo['Labels'],
} as NetworkInspectInfo);
expect(network.config.labels).to.deep.equal({
'io.balena.features.something': '123',
'io.balena.features.dummy': 'abc',
});
});
});
describe('creating a network compose configuration from a network instance', () => {
it('creates a docker compose network object from the internal network config', () => {
const network = Network.fromDockerNetwork({
Id: 'deadbeef',
Name: '1234_default',
Driver: 'bridge',
EnableIPv6: true,
IPAM: {
Driver: 'default',
Options: {},
Config: [
{
Subnet: '172.18.0.0/16',
Gateway: '172.18.0.1',
},
],
} as NetworkInspectInfo['IPAM'],
Internal: true,
Containers: {},
Options: {
'com.docker.some-option': 'abcd',
} as NetworkInspectInfo['Options'],
Labels: {
'io.balena.features.something': '123',
} as NetworkInspectInfo['Labels'],
} as NetworkInspectInfo);
// Convert to compose object
const compose = network.toComposeObject();
expect(compose.driver).to.equal('bridge');
expect(compose.driver_opts).to.deep.equal({
'com.docker.some-option': 'abcd',
});
expect(compose.enable_ipv6).to.equal(true);
expect(compose.internal).to.equal(true);
expect(compose.ipam).to.deep.equal({
driver: 'default',
options: {},
config: [
{
subnet: '172.18.0.0/16',
gateway: '172.18.0.1',
},
],
});
expect(compose.labels).to.deep.equal({
'io.balena.features.something': '123',
});
});
});
describe('generateDockerName', () => {
it('creates a proper network name from the user given name and the app id', () => {
expect(Network.generateDockerName(12345, 'default')).to.equal(
'12345_default',
);
expect(Network.generateDockerName(12345, 'bleh')).to.equal('12345_bleh');
expect(Network.generateDockerName(1, 'default')).to.equal('1_default');
});
});
describe('comparing network configurations', () => {
it('ignores IPAM configuration', () => {
const network = Network.fromComposeObject('default', 12345, {
ipam: {
driver: 'default',
config: [
{
subnet: '172.20.0.0/16',
ip_range: '172.20.10.0/24',
gateway: '172.20.0.1',
},
],
options: {},
},
});
expect(
network.isEqualConfig(Network.fromComposeObject('default', 12345, {})),
).to.be.true;
// Only ignores ipam.config, not other ipam elements
expect(
network.isEqualConfig(
Network.fromComposeObject('default', 12345, {
ipam: { driver: 'aaa' },
}),
),
).to.be.false;
});
it('compares configurations recursively', () => {
expect(
Network.fromComposeObject('default', 12345, {}).isEqualConfig(
Network.fromComposeObject('default', 12345, {}),
),
).to.be.true;
expect(
Network.fromComposeObject('default', 12345, {
driver: 'default',
}).isEqualConfig(Network.fromComposeObject('default', 12345, {})),
).to.be.false;
expect(
Network.fromComposeObject('default', 12345, {
enable_ipv6: true,
}).isEqualConfig(Network.fromComposeObject('default', 12345, {})),
).to.be.false;
expect(
Network.fromComposeObject('default', 12345, {
enable_ipv6: false,
internal: false,
}).isEqualConfig(
Network.fromComposeObject('default', 12345, { internal: true }),
),
).to.be.false;
});
});
});

View File

@ -81,6 +81,12 @@ export function registerOverride<
overrides[name] = fn;
}
export function restoreOverride<T extends DockerodeFunction>(name: T) {
if (overrides.hasOwnProperty(name)) {
delete overrides[name];
}
}
export interface TestData {
networks: Dictionary<any>;
images: Dictionary<any>;
@ -201,6 +207,24 @@ function createMockedDockerode(data: TestData) {
return mockedDockerode;
}
type Prototype = Dictionary<(...args: any[]) => any>;
function clonePrototype(prototype: Prototype): Prototype {
const clone: Prototype = {};
Object.getOwnPropertyNames(prototype).forEach((fn) => {
if (fn !== 'constructor' && _.isFunction(prototype[fn])) {
clone[fn] = prototype[fn];
}
});
return clone;
}
function assignPrototype(target: Prototype, source: Prototype) {
Object.keys(source).forEach((fn) => {
target[fn] = source[fn];
});
}
export async function testWithData(
data: Partial<TestData>,
test: () => Promise<any>,
@ -216,7 +240,7 @@ export async function testWithData(
};
// grab the original prototype...
const basePrototype = dockerode.prototype;
const basePrototype = clonePrototype(dockerode.prototype);
// @ts-expect-error setting a RO property
dockerode.prototype = createMockedDockerode(mockedData);
@ -226,7 +250,6 @@ export async function testWithData(
await test();
} finally {
// reset the original prototype...
// @ts-expect-error setting a RO property
dockerode.prototype = basePrototype;
assignPrototype(dockerode.prototype, basePrototype);
}
}