Bump dockerode types to 2.5.34

This commit updates dockerode types to the latest 2.x version, removing the need
for custom composer types for network.

This commit also modifies network tests to use the new types

Change-type: minor
This commit is contained in:
Felipe Lalanne 2021-04-26 12:06:04 -04:00
parent c70aedf044
commit 95fb568aae
6 changed files with 275 additions and 256 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

@ -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;
});
});
});