mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-01 08:47:56 +00:00
Merge pull request #1676 from balena-os/test-improvements
Bump dockerode types to 2.5.34
This commit is contained in:
commit
404f5c77d7
6
package-lock.json
generated
6
package-lock.json
generated
@ -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": "*"
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
@ -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 () => {
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user