mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-25 16:31:05 +00:00
f558be0a16
This eliminates chances of host-Docker address collision for apps such as the Supervisor where all services have host networking. Closes: #2062 Change-type: patch Signed-off-by: Christina Ying Wang <christina@balena.io>
520 lines
13 KiB
TypeScript
520 lines
13 KiB
TypeScript
import { expect } from 'chai';
|
|
import * as sinon from 'sinon';
|
|
|
|
import { Network } from '~/src/compose/network';
|
|
import { NetworkInspectInfo } from '~/src/compose/types/network';
|
|
|
|
import { log } from '~/lib/supervisor-console';
|
|
|
|
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,
|
|
'deadbeef',
|
|
{},
|
|
);
|
|
|
|
expect(network.name).to.equal('default');
|
|
expect(network.appId).to.equal(12345);
|
|
expect(network.appUuid).to.equal('deadbeef');
|
|
|
|
// Default configuration options
|
|
expect(network.config.driver).to.equal('bridge');
|
|
expect(network.config.ipam).to.deep.equal({
|
|
driver: 'default',
|
|
config: [],
|
|
options: {},
|
|
});
|
|
expect(network.config.enableIPv6).to.equal(false);
|
|
expect(network.config.labels).to.deep.equal({
|
|
'io.balena.app-id': '12345',
|
|
});
|
|
expect(network.config.options).to.deep.equal({});
|
|
expect(network.config.configOnly).to.equal(false);
|
|
});
|
|
|
|
it('normalizes legacy labels', () => {
|
|
const network = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
labels: {
|
|
'io.resin.features.something': '1234',
|
|
},
|
|
});
|
|
|
|
expect(network.config.labels).to.deep.equal({
|
|
'io.balena.features.something': '1234',
|
|
'io.balena.app-id': '12345',
|
|
});
|
|
});
|
|
|
|
it('accepts valid IPAM configurations', () => {
|
|
const network0 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
ipam: { driver: 'dummy', config: [], options: {} },
|
|
});
|
|
|
|
// Default configuration options
|
|
expect(network0.config.ipam).to.deep.equal({
|
|
driver: 'dummy',
|
|
config: [],
|
|
options: {},
|
|
});
|
|
|
|
const network1 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
ipam: {
|
|
driver: 'default',
|
|
config: [
|
|
{
|
|
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',
|
|
},
|
|
],
|
|
options: {},
|
|
},
|
|
});
|
|
|
|
// Default configuration options
|
|
expect(network1.config.ipam).to.deep.equal({
|
|
driver: 'default',
|
|
config: [
|
|
{
|
|
subnet: '172.20.0.0/16',
|
|
ipRange: '172.20.10.0/24',
|
|
gateway: '172.20.0.1',
|
|
auxAddress: { host0: '172.20.10.15', host1: '172.20.10.16' },
|
|
},
|
|
],
|
|
options: {},
|
|
});
|
|
});
|
|
|
|
it('warns about IPAM configuration without both gateway and subnet', () => {
|
|
const logStub = log.warn as sinon.SinonStub;
|
|
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
ipam: {
|
|
driver: 'default',
|
|
config: [
|
|
{
|
|
subnet: '172.20.0.0/16',
|
|
},
|
|
],
|
|
options: {},
|
|
},
|
|
});
|
|
|
|
expect(logStub).to.have.been.calledOnce;
|
|
expect(logStub).to.have.been.calledWithMatch(
|
|
'Network IPAM config entries must have both a subnet and gateway',
|
|
);
|
|
|
|
logStub.resetHistory();
|
|
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
ipam: {
|
|
driver: 'default',
|
|
config: [
|
|
{
|
|
gateway: '172.20.0.1',
|
|
},
|
|
],
|
|
options: {},
|
|
},
|
|
});
|
|
|
|
expect(logStub).to.have.been.calledOnce;
|
|
expect(logStub).to.have.been.calledWithMatch(
|
|
'Network IPAM config entries must have both a subnet and gateway',
|
|
);
|
|
});
|
|
|
|
it('parses values from a compose object', () => {
|
|
const network1 = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
driver: 'bridge',
|
|
enable_ipv6: true,
|
|
internal: false,
|
|
ipam: {
|
|
driver: 'default',
|
|
options: {
|
|
'com.docker.ipam-option': 'abcd',
|
|
},
|
|
config: [
|
|
{
|
|
subnet: '172.18.0.0/16',
|
|
gateway: '172.18.0.1',
|
|
},
|
|
],
|
|
},
|
|
driver_opts: {
|
|
'com.docker.network-option': 'abcd',
|
|
},
|
|
labels: {
|
|
'com.docker.some-label': 'yes',
|
|
},
|
|
config_only: true,
|
|
});
|
|
|
|
const dockerConfig = network1.toDockerConfig();
|
|
|
|
expect(dockerConfig.Driver).to.equal('bridge');
|
|
// Check duplicate forced to be true
|
|
expect(dockerConfig.CheckDuplicate).to.equal(true);
|
|
expect(dockerConfig.Internal).to.equal(false);
|
|
expect(dockerConfig.EnableIPv6).to.equal(true);
|
|
|
|
expect(dockerConfig.IPAM).to.deep.equal({
|
|
Driver: 'default',
|
|
Options: {
|
|
'com.docker.ipam-option': 'abcd',
|
|
},
|
|
Config: [
|
|
{
|
|
Subnet: '172.18.0.0/16',
|
|
Gateway: '172.18.0.1',
|
|
},
|
|
],
|
|
});
|
|
// If ConfigOnly is true, Subnet & Gateway remain in config
|
|
// but no actual networks are created.
|
|
expect(dockerConfig.ConfigOnly).to.equal(true);
|
|
expect(dockerConfig.Labels).to.deep.equal({
|
|
'io.balena.supervised': 'true',
|
|
'io.balena.app-id': '12345',
|
|
'com.docker.some-label': 'yes',
|
|
});
|
|
|
|
expect(dockerConfig.Options).to.deep.equal({
|
|
'com.docker.network-option': 'abcd',
|
|
});
|
|
});
|
|
});
|
|
|
|
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();
|
|
|
|
expect(() =>
|
|
Network.fromDockerNetwork({
|
|
Id: 'deadbeef',
|
|
Name: 'a173bdb734884b778f5cc3dffd18733e_default',
|
|
Labels: {}, // no app-id
|
|
} as NetworkInspectInfo),
|
|
).to.throw();
|
|
});
|
|
|
|
it('creates a network object from a legacy 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.supervised': 'true',
|
|
'io.balena.features.something': '123',
|
|
} as NetworkInspectInfo['Labels'],
|
|
ConfigOnly: false,
|
|
} 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',
|
|
});
|
|
expect(network.config.configOnly).to.equal(false);
|
|
});
|
|
|
|
it('creates a network object from a docker network configuration', () => {
|
|
const network = Network.fromDockerNetwork({
|
|
Id: 'deadbeef',
|
|
Name: 'a173bdb734884b778f5cc3dffd18733e_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.supervised': 'true',
|
|
'io.balena.features.something': '123',
|
|
'io.balena.app-id': '1234',
|
|
} as NetworkInspectInfo['Labels'],
|
|
ConfigOnly: false,
|
|
} as NetworkInspectInfo);
|
|
|
|
expect(network.appId).to.equal(1234);
|
|
expect(network.appUuid).to.equal('a173bdb734884b778f5cc3dffd18733e');
|
|
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',
|
|
'io.balena.app-id': '1234',
|
|
});
|
|
expect(network.config.configOnly).to.equal(false);
|
|
});
|
|
|
|
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: 'a173bdb734884b778f5cc3dffd18733e_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',
|
|
'io.balena.app-id': '12345',
|
|
} as NetworkInspectInfo['Labels'],
|
|
ConfigOnly: true,
|
|
} as NetworkInspectInfo);
|
|
|
|
expect(network.appId).to.equal(12345);
|
|
expect(network.appUuid).to.equal('a173bdb734884b778f5cc3dffd18733e');
|
|
|
|
// 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',
|
|
'io.balena.app-id': '12345',
|
|
});
|
|
expect(compose.config_only).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('generateDockerName', () => {
|
|
it('creates a proper network name from the user given name and the app uuid', () => {
|
|
expect(Network.generateDockerName('deadbeef', 'default')).to.equal(
|
|
'deadbeef_default',
|
|
);
|
|
expect(Network.generateDockerName('deadbeef', 'bleh')).to.equal(
|
|
'deadbeef_bleh',
|
|
);
|
|
expect(Network.generateDockerName(1, 'default')).to.equal('1_default');
|
|
});
|
|
});
|
|
|
|
describe('comparing network configurations', () => {
|
|
it('ignores IPAM configuration', () => {
|
|
const network = Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
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, 'deadbeef', {}),
|
|
),
|
|
).to.be.true;
|
|
|
|
// Only ignores ipam.config, not other ipam elements
|
|
expect(
|
|
network.isEqualConfig(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
ipam: { driver: 'aaa' },
|
|
}),
|
|
),
|
|
).to.be.false;
|
|
});
|
|
|
|
it('compares configurations recursively', () => {
|
|
expect(
|
|
Network.fromComposeObject(
|
|
'default',
|
|
12345,
|
|
'deadbeef',
|
|
{},
|
|
).isEqualConfig(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
|
),
|
|
).to.be.true;
|
|
expect(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
driver: 'default',
|
|
}).isEqualConfig(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
|
),
|
|
).to.be.false;
|
|
expect(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
enable_ipv6: true,
|
|
}).isEqualConfig(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {}),
|
|
),
|
|
).to.be.false;
|
|
expect(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
enable_ipv6: false,
|
|
internal: false,
|
|
}).isEqualConfig(
|
|
Network.fromComposeObject('default', 12345, 'deadbeef', {
|
|
internal: true,
|
|
}),
|
|
),
|
|
).to.be.false;
|
|
|
|
// Comparison of a network without the app-uuid and a network
|
|
// with uuid has to return false
|
|
expect(
|
|
Network.fromComposeObject(
|
|
'default',
|
|
12345,
|
|
'deadbeef',
|
|
{},
|
|
).isEqualConfig(
|
|
Network.fromDockerNetwork({
|
|
Id: 'deadbeef',
|
|
Name: '12345_default',
|
|
IPAM: {
|
|
Driver: 'default',
|
|
Options: {},
|
|
Config: [],
|
|
} as NetworkInspectInfo['IPAM'],
|
|
Labels: {
|
|
'io.balena.supervised': 'true',
|
|
} as NetworkInspectInfo['Labels'],
|
|
} as NetworkInspectInfo),
|
|
),
|
|
).to.be.false;
|
|
});
|
|
});
|
|
});
|