balena-supervisor/test/unit/compose/ports.spec.ts
Felipe Lalanne 416170bc05 Ignore expose service compose configuration
The docker EXPOSE directive and corresponding docker-compose `expose`
service configuration serves as documentation/metadata that a container
listens on a certain port that may be used for service discovery but it doesn't
have any real impact on the ability for
other containers on the same network to access the exposed service via
the port. In newer engine implementations, this property may conflict
with other network configurations, and prevent the container from being
started by the docker engine (see #2211).

This PR removes code that would manage the expose property and takes the
property out of the whitelist. A composition with the `expose` property
will result in the log message `Ignoring unsupported or unknown compose fields: expose`.

While this change should not have operational impact, it still removes
a previously supported configuration and as such there is a chance of it
being a breaking change for some applications. For this reason it is
being published as a new major version.

Change-type: major
Closes: #2211
2023-10-23 11:41:32 -03:00

388 lines
9.8 KiB
TypeScript

import { PortMap, PortRange } from '~/src/compose/ports';
import { expect } from 'chai';
// Force cast `PortMap` as a public version so we can test it
const PortMapPublic = PortMap as any as new (
portStrOrObj: string | PortRange,
) => PortMap;
describe('compose/ports', function () {
describe('Port string parsing', function () {
it('should correctly parse a port string without a range', function () {
expect(new PortMapPublic('80')).to.deep.equal(
new PortMapPublic({
internalStart: 80,
internalEnd: 80,
externalStart: 80,
externalEnd: 80,
protocol: 'tcp',
host: '',
}),
);
expect(new PortMapPublic('80:80')).to.deep.equal(
new PortMapPublic({
internalStart: 80,
internalEnd: 80,
externalStart: 80,
externalEnd: 80,
protocol: 'tcp',
host: '',
}),
);
});
it('should correctly parse a port string without an external range', () =>
expect(new PortMapPublic('80-90')).to.deep.equal(
new PortMapPublic({
internalStart: 80,
internalEnd: 90,
externalStart: 80,
externalEnd: 90,
protocol: 'tcp',
host: '',
}),
));
it('should correctly parse a port string with a range', () =>
expect(new PortMapPublic('80-100:100-120')).to.deep.equal(
new PortMapPublic({
internalStart: 100,
internalEnd: 120,
externalStart: 80,
externalEnd: 100,
protocol: 'tcp',
host: '',
}),
));
it('should correctly parse a protocol', function () {
expect(new PortMapPublic('80/udp')).to.deep.equal(
new PortMapPublic({
internalStart: 80,
internalEnd: 80,
externalStart: 80,
externalEnd: 80,
protocol: 'udp',
host: '',
}),
);
expect(new PortMapPublic('80:80/udp')).to.deep.equal(
new PortMapPublic({
internalStart: 80,
internalEnd: 80,
externalStart: 80,
externalEnd: 80,
protocol: 'udp',
host: '',
}),
);
expect(new PortMapPublic('80-90:100-110/udp')).to.deep.equal(
new PortMapPublic({
internalStart: 100,
internalEnd: 110,
externalStart: 80,
externalEnd: 90,
protocol: 'udp',
host: '',
}),
);
});
it('should throw when the port string is incorrect', () =>
expect(() => new PortMapPublic('80-90:80-85')).to.throw);
});
describe('toDockerOpts', function () {
it('should correctly generate docker options', () =>
expect(new PortMapPublic('80').toDockerOpts()).to.deep.equal({
portBindings: {
'80/tcp': [{ HostIp: '', HostPort: '80' }],
},
}));
it('should correctly generate docker options for a port range', () =>
expect(new PortMapPublic('80-85').toDockerOpts()).to.deep.equal({
portBindings: {
'80/tcp': [{ HostIp: '', HostPort: '80' }],
'81/tcp': [{ HostIp: '', HostPort: '81' }],
'82/tcp': [{ HostIp: '', HostPort: '82' }],
'83/tcp': [{ HostIp: '', HostPort: '83' }],
'84/tcp': [{ HostIp: '', HostPort: '84' }],
'85/tcp': [{ HostIp: '', HostPort: '85' }],
},
}));
});
describe('fromDockerOpts', function () {
it('should correctly detect a port range', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [{ HostIp: '123', HostPort: '200' }],
'101/tcp': [{ HostIp: '123', HostPort: '201' }],
'102/tcp': [{ HostIp: '123', HostPort: '202' }],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 102,
externalStart: 200,
externalEnd: 202,
protocol: 'tcp',
host: '123',
}),
]));
it('should correctly split ports into ranges', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [{ HostIp: '123', HostPort: '200' }],
'101/tcp': [{ HostIp: '123', HostPort: '201' }],
'105/tcp': [{ HostIp: '123', HostPort: '205' }],
'106/tcp': [{ HostIp: '123', HostPort: '206' }],
'110/tcp': [{ HostIp: '123', HostPort: '210' }],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 101,
externalStart: 200,
externalEnd: 201,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 105,
internalEnd: 106,
externalStart: 205,
externalEnd: 206,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 110,
internalEnd: 110,
externalStart: 210,
externalEnd: 210,
protocol: 'tcp',
host: '123',
}),
]));
it('should correctly consider internal and external ports', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [{ HostIp: '123', HostPort: '200' }],
'101/tcp': [{ HostIp: '123', HostPort: '101' }],
'102/tcp': [{ HostIp: '123', HostPort: '202' }],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 100,
externalStart: 200,
externalEnd: 200,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 101,
internalEnd: 101,
externalStart: 101,
externalEnd: 101,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 102,
internalEnd: 102,
externalStart: 202,
externalEnd: 202,
protocol: 'tcp',
host: '123',
}),
]));
it('should consider the host when generating ranges', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [{ HostIp: '123', HostPort: '200' }],
'101/tcp': [{ HostIp: '456', HostPort: '201' }],
'102/tcp': [{ HostIp: '456', HostPort: '202' }],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 100,
externalStart: 200,
externalEnd: 200,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 101,
internalEnd: 102,
externalStart: 201,
externalEnd: 202,
protocol: 'tcp',
host: '456',
}),
]));
it('should consider the protocol when generating ranges', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [{ HostIp: '123', HostPort: '200' }],
'101/udp': [{ HostIp: '123', HostPort: '201' }],
'102/udp': [{ HostIp: '123', HostPort: '202' }],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 100,
externalStart: 200,
externalEnd: 200,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 101,
internalEnd: 102,
externalStart: 201,
externalEnd: 202,
protocol: 'udp',
host: '123',
}),
]));
it('should correctly detect multiple hosts ports on an internal port', () =>
expect(
PortMap.fromDockerOpts({
'100/tcp': [
{ HostIp: '123', HostPort: '200' },
{ HostIp: '123', HostPort: '201' },
],
}),
).to.deep.equal([
new PortMapPublic({
internalStart: 100,
internalEnd: 100,
externalStart: 200,
externalEnd: 200,
protocol: 'tcp',
host: '123',
}),
new PortMapPublic({
internalStart: 100,
internalEnd: 100,
externalStart: 201,
externalEnd: 201,
protocol: 'tcp',
host: '123',
}),
]));
});
describe('Running container comparison', () =>
it('should not consider order when comparing current and target state', function () {
const portBindings = require('~/test-data/ports/not-ascending/port-bindings.json');
const compose = require('~/test-data/ports/not-ascending/compose.json');
const portMapsCurrent = PortMap.fromDockerOpts(portBindings);
const portMapsTarget = PortMap.fromComposePorts(compose.ports);
expect(portMapsTarget).to.deep.equal(portMapsCurrent);
}));
describe('fromComposePorts', () =>
it('should normalise compose ports', () =>
expect(
PortMap.fromComposePorts(['80:80', '81:81', '82:82']),
).to.deep.equal([new PortMapPublic('80-82')])));
describe('normalisePortMaps', function () {
it('should correctly normalise PortMap lists', function () {
expect(
PortMap.normalisePortMaps([
new PortMapPublic('80:90'),
new PortMapPublic('81:91'),
]),
).to.deep.equal([new PortMapPublic('80-81:90-91')]);
expect(
PortMap.normalisePortMaps([
new PortMapPublic('80:90'),
new PortMapPublic('81:91'),
new PortMapPublic('82:92'),
new PortMapPublic('83:93'),
new PortMapPublic('84:94'),
new PortMapPublic('85:95'),
new PortMapPublic('86:96'),
new PortMapPublic('87:97'),
new PortMapPublic('88:98'),
new PortMapPublic('89:99'),
new PortMapPublic('90:100'),
]),
).to.deep.equal([new PortMapPublic('80-90:90-100')]);
expect(PortMap.normalisePortMaps([])).to.deep.equal([]);
});
it('should correctly consider protocols', function () {
expect(
PortMap.normalisePortMaps([
new PortMapPublic('80:90'),
new PortMapPublic('81:91/udp'),
]),
).to.deep.equal([
new PortMapPublic('80:90'),
new PortMapPublic('81:91/udp'),
]);
expect(
PortMap.normalisePortMaps([
new PortMapPublic('80:90'),
new PortMapPublic('100:110/udp'),
new PortMapPublic('81:91'),
]),
).to.deep.equal([
new PortMapPublic('80-81:90-91'),
new PortMapPublic('100:110/udp'),
]);
// This shouldn't ever be provided, but it shows the algorithm
// working properly
expect(
PortMap.normalisePortMaps([
new PortMapPublic('80:90'),
new PortMapPublic('81:91/udp'),
new PortMapPublic('81:91'),
]),
).to.deep.equal([
new PortMapPublic('80-81:90-91'),
new PortMapPublic('81:91/udp'),
]);
});
it('should correctly consider hosts', function () {
expect(
PortMap.normalisePortMaps([
new PortMapPublic('127.0.0.1:80:80'),
new PortMapPublic('81:81'),
]),
).to.deep.equal([
new PortMapPublic('127.0.0.1:80:80'),
new PortMapPublic('81:81'),
]);
expect(
PortMap.normalisePortMaps([
new PortMapPublic('127.0.0.1:80:80'),
new PortMapPublic('127.0.0.1:81:81'),
]),
).to.deep.equal([new PortMapPublic('127.0.0.1:80-81:80-81')]);
});
});
});