mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-12 16:02:57 +00:00
cb98133717
When getting the service from the docker container, remove the containerId from the list of aliases (which gets added by docker). This will make it easier to use the current service state as a target. This will help us remove the `safeStateClone` function in the API in a future commit Change-type: patch
1398 lines
38 KiB
TypeScript
1398 lines
38 KiB
TypeScript
import * as _ from 'lodash';
|
|
|
|
import { expect } from 'chai';
|
|
import { createContainer } from '~/test-lib/mockerode';
|
|
|
|
import Service from '~/src/compose/service';
|
|
import Volume from '~/src/compose/volume';
|
|
import * as ServiceT from '~/src/compose/types/service';
|
|
import * as constants from '~/lib/constants';
|
|
|
|
const configs = {
|
|
simple: {
|
|
compose: require('~/test-data/docker-states/simple/compose.json'),
|
|
imageInfo: require('~/test-data/docker-states/simple/imageInfo.json'),
|
|
inspect: require('~/test-data/docker-states/simple/inspect.json'),
|
|
},
|
|
entrypoint: {
|
|
compose: require('~/test-data/docker-states/entrypoint/compose.json'),
|
|
imageInfo: require('~/test-data/docker-states/entrypoint/imageInfo.json'),
|
|
inspect: require('~/test-data/docker-states/entrypoint/inspect.json'),
|
|
},
|
|
networkModeService: {
|
|
compose: require('~/test-data/docker-states/network-mode-service/compose.json'),
|
|
imageInfo: require('~/test-data/docker-states/network-mode-service/imageInfo.json'),
|
|
inspect: require('~/test-data/docker-states/network-mode-service/inspect.json'),
|
|
},
|
|
};
|
|
|
|
describe('compose/service: unit tests', () => {
|
|
describe('Creating a service instance from a compose object', () => {
|
|
it('extends environment variables with additional OS info', async () => {
|
|
const extendEnvVarsOpts = {
|
|
uuid: '1234',
|
|
appName: 'awesomeApp',
|
|
commit: 'abcdef',
|
|
name: 'awesomeDevice',
|
|
version: 'v1.0.0',
|
|
deviceArch: 'amd64',
|
|
deviceType: 'raspberry-pi',
|
|
osVersion: 'Resin OS 2.0.2',
|
|
};
|
|
const service = {
|
|
appId: '23',
|
|
appUuid: 'deadbeef',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
serviceName: 'serviceName',
|
|
environment: {
|
|
FOO: 'bar',
|
|
A_VARIABLE: 'ITS_VALUE',
|
|
},
|
|
};
|
|
const s = await Service.fromComposeObject(
|
|
service,
|
|
extendEnvVarsOpts as any,
|
|
);
|
|
|
|
expect(s.config.environment).to.deep.equal({
|
|
FOO: 'bar',
|
|
A_VARIABLE: 'ITS_VALUE',
|
|
RESIN_APP_ID: '23',
|
|
RESIN_APP_UUID: 'deadbeef',
|
|
RESIN_APP_NAME: 'awesomeApp',
|
|
RESIN_DEVICE_UUID: '1234',
|
|
RESIN_DEVICE_ARCH: 'amd64',
|
|
RESIN_DEVICE_TYPE: 'raspberry-pi',
|
|
RESIN_HOST_OS_VERSION: 'Resin OS 2.0.2',
|
|
RESIN_SERVICE_NAME: 'serviceName',
|
|
RESIN_APP_LOCK_PATH: '/tmp/balena/updates.lock',
|
|
RESIN_SERVICE_KILL_ME_PATH: '/tmp/balena/handover-complete',
|
|
RESIN: '1',
|
|
BALENA_APP_ID: '23',
|
|
BALENA_APP_UUID: 'deadbeef',
|
|
BALENA_APP_NAME: 'awesomeApp',
|
|
BALENA_DEVICE_UUID: '1234',
|
|
BALENA_DEVICE_ARCH: 'amd64',
|
|
BALENA_DEVICE_TYPE: 'raspberry-pi',
|
|
BALENA_HOST_OS_VERSION: 'Resin OS 2.0.2',
|
|
BALENA_SERVICE_NAME: 'serviceName',
|
|
BALENA_APP_LOCK_PATH: '/tmp/balena/updates.lock',
|
|
BALENA_SERVICE_HANDOVER_COMPLETE_PATH: '/tmp/balena/handover-complete',
|
|
BALENA: '1',
|
|
USER: 'root',
|
|
});
|
|
});
|
|
|
|
it('returns the correct default bind mounts', async () => {
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
},
|
|
{ appName: 'foo' } as any,
|
|
);
|
|
const binds = (Service as any).defaultBinds(s.appId, s.serviceName);
|
|
expect(binds).to.deep.equal([
|
|
'/tmp/balena-supervisor/services/1234/foo:/tmp/resin',
|
|
'/tmp/balena-supervisor/services/1234/foo:/tmp/balena',
|
|
]);
|
|
});
|
|
|
|
it('produces the correct port bindings and exposed ports', async () => {
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
expose: [1000, '243/udp'],
|
|
ports: ['2344', '2345:2354', '2346:2367/udp'],
|
|
},
|
|
},
|
|
{
|
|
imageInfo: {
|
|
Config: {
|
|
ExposedPorts: {
|
|
'53/tcp': {},
|
|
'53/udp': {},
|
|
'2354/tcp': {},
|
|
},
|
|
},
|
|
},
|
|
} as any,
|
|
);
|
|
|
|
const ports = (s as any).generateExposeAndPorts();
|
|
expect(ports.portBindings).to.deep.equal({
|
|
'2344/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '2344',
|
|
},
|
|
],
|
|
'2354/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '2345',
|
|
},
|
|
],
|
|
'2367/udp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '2346',
|
|
},
|
|
],
|
|
});
|
|
expect(ports.exposedPorts).to.deep.equal({
|
|
'1000/tcp': {},
|
|
'243/udp': {},
|
|
'2344/tcp': {},
|
|
'2354/tcp': {},
|
|
'2367/udp': {},
|
|
'53/tcp': {},
|
|
'53/udp': {},
|
|
});
|
|
});
|
|
|
|
it('correctly handles port ranges', async () => {
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
expose: [1000, '243/udp'],
|
|
ports: ['1000-1003:2000-2003'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
const ports = (s as any).generateExposeAndPorts();
|
|
expect(ports.portBindings).to.deep.equal({
|
|
'2000/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '1000',
|
|
},
|
|
],
|
|
'2001/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '1001',
|
|
},
|
|
],
|
|
'2002/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '1002',
|
|
},
|
|
],
|
|
'2003/tcp': [
|
|
{
|
|
HostIp: '',
|
|
HostPort: '1003',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(ports.exposedPorts).to.deep.equal({
|
|
'1000/tcp': {},
|
|
'2000/tcp': {},
|
|
'2001/tcp': {},
|
|
'2002/tcp': {},
|
|
'2003/tcp': {},
|
|
'243/udp': {},
|
|
});
|
|
});
|
|
|
|
it('should correctly handle large port ranges', async function () {
|
|
this.timeout(60000);
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
ports: ['5-65536:5-65536/tcp', '5-65536:5-65536/udp'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect((s as any).generateExposeAndPorts()).to.not.throw;
|
|
});
|
|
|
|
it('should correctly report implied exposed ports from portMappings', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'test',
|
|
composition: {
|
|
ports: ['80:80', '100:100'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config)
|
|
.to.have.property('expose')
|
|
.that.deep.equals(['80/tcp', '100/tcp']);
|
|
});
|
|
|
|
it('should correctly handle spaces in volume definitions', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123,
|
|
serviceId: 123,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: [
|
|
'vol1:vol2',
|
|
'vol3 :/usr/src/app',
|
|
'vol4: /usr/src/app',
|
|
'vol5 : vol6',
|
|
],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config)
|
|
.to.have.property('volumes')
|
|
.that.deep.equals([
|
|
`${Volume.generateDockerName(123, 'vol1')}:vol2`,
|
|
`${Volume.generateDockerName(123, 'vol3')}:/usr/src/app`,
|
|
`${Volume.generateDockerName(123, 'vol4')}:/usr/src/app`,
|
|
`${Volume.generateDockerName(123, 'vol5')}:vol6`,
|
|
|
|
'/tmp/balena-supervisor/services/123/test:/tmp/resin',
|
|
'/tmp/balena-supervisor/services/123/test:/tmp/balena',
|
|
]);
|
|
});
|
|
|
|
describe('Parsing memory strings from compose configuration', () => {
|
|
const makeComposeServiceWithLimit = async (memLimit?: string | number) =>
|
|
await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
composition: {
|
|
mem_limit: memLimit,
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
it('should correctly parse memory number strings without a unit', async () =>
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64')).config.memLimit,
|
|
).to.equal(64));
|
|
|
|
it('should correctly apply the default value', async () =>
|
|
expect(
|
|
(await makeComposeServiceWithLimit(undefined)).config.memLimit,
|
|
).to.equal(0));
|
|
|
|
it('should correctly support parsing numbers as memory limits', async () =>
|
|
expect(
|
|
(await makeComposeServiceWithLimit(64)).config.memLimit,
|
|
).to.equal(64));
|
|
|
|
it('should correctly parse memory number strings that use a byte unit', async () => {
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64b')).config.memLimit,
|
|
).to.equal(64);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64B')).config.memLimit,
|
|
).to.equal(64);
|
|
});
|
|
|
|
it('should correctly parse memory number strings that use a kilobyte unit', async () => {
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64k')).config.memLimit,
|
|
).to.equal(65536);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64K')).config.memLimit,
|
|
).to.equal(65536);
|
|
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64kb')).config.memLimit,
|
|
).to.equal(65536);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64Kb')).config.memLimit,
|
|
).to.equal(65536);
|
|
});
|
|
|
|
it('should correctly parse memory number strings that use a megabyte unit', async () => {
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64m')).config.memLimit,
|
|
).to.equal(67108864);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64M')).config.memLimit,
|
|
).to.equal(67108864);
|
|
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64mb')).config.memLimit,
|
|
).to.equal(67108864);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64Mb')).config.memLimit,
|
|
).to.equal(67108864);
|
|
});
|
|
|
|
it('should correctly parse memory number strings that use a gigabyte unit', async () => {
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64g')).config.memLimit,
|
|
).to.equal(68719476736);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64G')).config.memLimit,
|
|
).to.equal(68719476736);
|
|
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64gb')).config.memLimit,
|
|
).to.equal(68719476736);
|
|
expect(
|
|
(await makeComposeServiceWithLimit('64Gb')).config.memLimit,
|
|
).to.equal(68719476736);
|
|
});
|
|
});
|
|
|
|
describe('Getting work dir from the compose configuration', () => {
|
|
const makeComposeServiceWithWorkdir = async (workdir?: string) =>
|
|
await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
composition: {
|
|
workingDir: workdir,
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
it('should remove a trailing slash', async () => {
|
|
expect(
|
|
(await makeComposeServiceWithWorkdir('/usr/src/app/')).config
|
|
.workingDir,
|
|
).to.equal('/usr/src/app');
|
|
expect(
|
|
(await makeComposeServiceWithWorkdir('/')).config.workingDir,
|
|
).to.equal('/');
|
|
expect(
|
|
(await makeComposeServiceWithWorkdir('/usr/src/app')).config
|
|
.workingDir,
|
|
).to.equal('/usr/src/app');
|
|
expect(
|
|
(await makeComposeServiceWithWorkdir('')).config.workingDir,
|
|
).to.equal('');
|
|
});
|
|
});
|
|
|
|
describe('Configuring service networks', () => {
|
|
it('should correctly convert networks from compose to docker format', async () => {
|
|
const makeComposeServiceWithNetwork = async (
|
|
networks: ServiceT.ServiceComposeConfig['networks'],
|
|
) =>
|
|
await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
appUuid: 'deadbeef',
|
|
serviceId: 123456,
|
|
serviceName: 'test',
|
|
composition: {
|
|
networks,
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(
|
|
(
|
|
await makeComposeServiceWithNetwork({
|
|
balena: {
|
|
ipv4Address: '1.2.3.4',
|
|
},
|
|
})
|
|
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
|
).to.deep.equal({
|
|
EndpointsConfig: {
|
|
deadbeef_balena: {
|
|
IPAMConfig: {
|
|
IPv4Address: '1.2.3.4',
|
|
},
|
|
Aliases: ['test'],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(
|
|
(
|
|
await makeComposeServiceWithNetwork({
|
|
balena: {
|
|
aliases: ['test', '1123'],
|
|
ipv4Address: '1.2.3.4',
|
|
ipv6Address: '5.6.7.8',
|
|
linkLocalIps: ['123.123.123'],
|
|
},
|
|
})
|
|
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
|
|
).to.deep.equal({
|
|
EndpointsConfig: {
|
|
deadbeef_balena: {
|
|
IPAMConfig: {
|
|
IPv4Address: '1.2.3.4',
|
|
IPv6Address: '5.6.7.8',
|
|
LinkLocalIPs: ['123.123.123'],
|
|
},
|
|
Aliases: ['test', '1123'],
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Comparing services', () => {
|
|
describe('Comparing array parameters', () => {
|
|
it('Should correctly compare ordered array parameters', async () => {
|
|
const svc1 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
dns: ['8.8.8.8', '1.1.1.1'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
let svc2 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
dns: ['8.8.8.8', '1.1.1.1'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
expect(svc1.isEqualConfig(svc2, {})).to.be.true;
|
|
|
|
svc2 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
dns: ['1.1.1.1', '8.8.8.8'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
expect(!svc1.isEqualConfig(svc2, {})).to.be.true;
|
|
});
|
|
|
|
it('should correctly compare non-ordered array parameters', async () => {
|
|
const svc1 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: ['abcdef', 'ghijk'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
let svc2 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: ['abcdef', 'ghijk'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
expect(svc1.isEqualConfig(svc2, {})).to.be.true;
|
|
|
|
svc2 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: ['ghijk', 'abcdef'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
expect(svc1.isEqualConfig(svc2, {})).to.be.true;
|
|
});
|
|
|
|
it('should correctly compare both ordered and non-ordered array parameters', async () => {
|
|
const svc1 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: ['abcdef', 'ghijk'],
|
|
dns: ['8.8.8.8', '1.1.1.1'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
const svc2 = await Service.fromComposeObject(
|
|
{
|
|
appId: 1,
|
|
serviceId: 1,
|
|
serviceName: 'test',
|
|
composition: {
|
|
volumes: ['ghijk', 'abcdef'],
|
|
dns: ['8.8.8.8', '1.1.1.1'],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
expect(svc1.isEqualConfig(svc2, {})).to.be.true;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Feature labels', () => {
|
|
describe('io.balena.features.balena-socket', () => {
|
|
it('should mount the socket in the container and set DOCKER_HOST with the proper location', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.balena-socket': '1',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.deep.include.members([
|
|
{
|
|
type: 'bind',
|
|
source: constants.dockerSocket,
|
|
target: constants.containerDockerSocket,
|
|
},
|
|
{
|
|
type: 'bind',
|
|
source: constants.dockerSocket,
|
|
target: constants.dockerSocket,
|
|
},
|
|
{
|
|
type: 'bind',
|
|
source: constants.dockerSocket,
|
|
target: '/var/run/balena.sock',
|
|
},
|
|
]);
|
|
|
|
expect(service.config.environment['DOCKER_HOST']).to.equal(
|
|
`unix://${constants.containerDockerSocket}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Features for mounting host directories (sys, dbus, proc, etc.)', () => {
|
|
it('should add /run/dbus:/host/run/dbus to bind mounts when io.balena.features.dbus is used', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.dbus': '1',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.include.members([
|
|
'/run/dbus:/host/run/dbus',
|
|
]);
|
|
});
|
|
|
|
it('should add `/sys` to the container bind mounts when io.balena.features.sysfs is used', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.sysfs': '1',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.include.members(['/sys:/sys']);
|
|
});
|
|
|
|
it('should add `/proc` to the container bind mounts when io.balena.features.procfs is used', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.procfs': '1',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.include.members(['/proc:/proc']);
|
|
});
|
|
|
|
it('should add `/lib/modules` to the container bind mounts when io.balena.features.kernel-modules is used (if the host path exists)', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.kernel-modules': '1',
|
|
},
|
|
},
|
|
{
|
|
appName: 'test',
|
|
hostPathExists: {
|
|
modules: true,
|
|
},
|
|
} as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.include.members([
|
|
'/lib/modules:/lib/modules',
|
|
]);
|
|
});
|
|
|
|
it('should NOT add `/lib/modules` to the container bind mounts when io.balena.features.kernel-modules is used (if the host path does NOT exist)', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.kernel-modules': '1',
|
|
},
|
|
},
|
|
{
|
|
appName: 'test',
|
|
hostPathExists: {
|
|
modules: false,
|
|
},
|
|
} as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.not.include.members([
|
|
'/lib/modules:/lib/modules',
|
|
]);
|
|
});
|
|
|
|
it('should add `/lib/firmware` to the container bind mounts when io.balena.features.firmware is used (if the host path exists)', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.firmware': '1',
|
|
},
|
|
},
|
|
{
|
|
appName: 'test',
|
|
hostPathExists: {
|
|
firmware: true,
|
|
},
|
|
} as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.include.members([
|
|
'/lib/firmware:/lib/firmware',
|
|
]);
|
|
});
|
|
|
|
it('should NOT add `/lib/firmware` to the container bind mounts when io.balena.features.firmware is used (if the host path does NOT exist)', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123456,
|
|
serviceId: 123456,
|
|
serviceName: 'foobar',
|
|
labels: {
|
|
'io.balena.features.firmware': '1',
|
|
},
|
|
},
|
|
{
|
|
appName: 'test',
|
|
hostPathExists: {
|
|
firmware: false,
|
|
},
|
|
} as any,
|
|
);
|
|
|
|
expect(service.config.volumes).to.not.include.members([
|
|
'/lib/firmware:/lib/firmware',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('io.balena.features.gpu', () => {
|
|
const gpuDeviceRequest = {
|
|
Driver: '',
|
|
DeviceIDs: [],
|
|
Count: 1,
|
|
Capabilities: [['gpu']],
|
|
Options: {},
|
|
};
|
|
it('should add GPU to compose configuration when the feature is set', async () => {
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: 123,
|
|
serviceId: 123,
|
|
serviceName: 'test',
|
|
labels: {
|
|
'io.balena.features.gpu': '1',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(s.config)
|
|
.to.have.property('deviceRequests')
|
|
.that.deep.equals([gpuDeviceRequest]);
|
|
});
|
|
|
|
it('should obtain the GPU config from docker configuration', () => {
|
|
const mockContainer = createContainer({
|
|
Id: 'deadbeef',
|
|
Name: 'main_431889_572579',
|
|
HostConfig: {
|
|
DeviceRequests: [gpuDeviceRequest],
|
|
},
|
|
Config: {
|
|
Labels: {
|
|
'io.resin.app-id': '1011165',
|
|
'io.resin.architecture': 'armv7hf',
|
|
'io.resin.service-id': '43697',
|
|
'io.resin.service-name': 'main',
|
|
'io.resin.supervised': 'true',
|
|
},
|
|
},
|
|
});
|
|
const s = Service.fromDockerContainer(mockContainer.inspectInfo);
|
|
|
|
expect(s.config)
|
|
.to.have.property('deviceRequests')
|
|
.that.deep.equals([gpuDeviceRequest]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Creating service instances from docker configuration', () => {
|
|
const omitConfigForComparison = (config: ServiceT.ServiceConfig) =>
|
|
_.omit(config, ['running', 'networks']);
|
|
|
|
it('should be equivalent to loading from compose config for simple services', async () => {
|
|
// TODO: improve the readability of this code
|
|
const composeSvc = await Service.fromComposeObject(
|
|
configs.simple.compose,
|
|
configs.simple.imageInfo,
|
|
);
|
|
const dockerSvc = Service.fromDockerContainer(configs.simple.inspect);
|
|
|
|
const composeConfig = omitConfigForComparison(composeSvc.config);
|
|
const dockerConfig = omitConfigForComparison(dockerSvc.config);
|
|
expect(composeConfig).to.deep.equal(dockerConfig);
|
|
|
|
expect(dockerSvc.isEqualConfig(composeSvc, {})).to.be.true;
|
|
});
|
|
|
|
it('should correctly handle a null entrypoint', async () => {
|
|
const composeSvc = await Service.fromComposeObject(
|
|
configs.entrypoint.compose,
|
|
configs.entrypoint.imageInfo,
|
|
);
|
|
const dockerSvc = Service.fromDockerContainer(configs.entrypoint.inspect);
|
|
|
|
const composeConfig = omitConfigForComparison(composeSvc.config);
|
|
const dockerConfig = omitConfigForComparison(dockerSvc.config);
|
|
expect(composeConfig).to.deep.equal(dockerConfig);
|
|
|
|
expect(dockerSvc.isEqualConfig(composeSvc, {})).to.equals(true);
|
|
});
|
|
|
|
describe('Networks', () => {
|
|
it('should correctly convert Docker format to service format', () => {
|
|
const { inspectInfo } = createContainer({
|
|
Id: 'deadbeef',
|
|
Name: 'main_431889_572579',
|
|
Config: {
|
|
Labels: {
|
|
'io.resin.app-id': '1011165',
|
|
'io.resin.architecture': 'armv7hf',
|
|
'io.resin.service-id': '43697',
|
|
'io.resin.service-name': 'main',
|
|
'io.resin.supervised': 'true',
|
|
},
|
|
},
|
|
});
|
|
|
|
const makeServiceFromDockerWithNetwork = (networks: {
|
|
[name: string]: any;
|
|
}) => {
|
|
return Service.fromDockerContainer({
|
|
...inspectInfo,
|
|
NetworkSettings: {
|
|
...inspectInfo.NetworkSettings,
|
|
Networks: networks,
|
|
},
|
|
});
|
|
};
|
|
|
|
expect(
|
|
makeServiceFromDockerWithNetwork({
|
|
'123456_balena': {
|
|
IPAMConfig: {
|
|
IPv4Address: '1.2.3.4',
|
|
},
|
|
Aliases: [],
|
|
},
|
|
}).config.networks,
|
|
).to.deep.equal({
|
|
'123456_balena': {
|
|
ipv4Address: '1.2.3.4',
|
|
},
|
|
});
|
|
|
|
expect(
|
|
makeServiceFromDockerWithNetwork({
|
|
'123456_balena': {
|
|
IPAMConfig: {
|
|
IPv4Address: '1.2.3.4',
|
|
IPv6Address: '5.6.7.8',
|
|
LinkLocalIps: ['123.123.123'],
|
|
},
|
|
Aliases: ['test', '1123', 'deadbeef'],
|
|
},
|
|
}).config.networks,
|
|
).to.deep.equal({
|
|
'123456_balena': {
|
|
ipv4Address: '1.2.3.4',
|
|
ipv6Address: '5.6.7.8',
|
|
linkLocalIps: ['123.123.123'],
|
|
// The container id got removed from the alias list
|
|
aliases: ['test', '1123'],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Network mode service:', () => {
|
|
const omitConfigForComparison = (config: ServiceT.ServiceConfig) =>
|
|
_.omit(config, ['running', 'networks']);
|
|
|
|
it('should correctly add a depends_on entry for the service', async () => {
|
|
let s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
network_mode: 'service: test',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(s.dependsOn).to.deep.equal(['test']);
|
|
|
|
s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
depends_on: ['another_service'],
|
|
network_mode: 'service: test',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(s.dependsOn).to.deep.equal(['another_service', 'test']);
|
|
});
|
|
|
|
it('should correctly convert a network_mode service: to a container:', async () => {
|
|
const s = await Service.fromComposeObject(
|
|
{
|
|
appId: '1234',
|
|
serviceName: 'foo',
|
|
releaseId: 2,
|
|
serviceId: 3,
|
|
imageId: 4,
|
|
composition: {
|
|
network_mode: 'service: test',
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
return expect(
|
|
s.toDockerContainer({
|
|
deviceName: '',
|
|
containerIds: { test: 'abcdef' },
|
|
}),
|
|
)
|
|
.to.have.property('HostConfig')
|
|
.that.has.property('NetworkMode')
|
|
.that.equals('container:abcdef');
|
|
});
|
|
|
|
it('should not cause a container restart if a service: container has not changed', async () => {
|
|
const composeSvc = await Service.fromComposeObject(
|
|
configs.networkModeService.compose,
|
|
configs.networkModeService.imageInfo,
|
|
);
|
|
const dockerSvc = Service.fromDockerContainer(
|
|
configs.networkModeService.inspect,
|
|
);
|
|
|
|
const composeConfig = omitConfigForComparison(composeSvc.config);
|
|
const dockerConfig = omitConfigForComparison(dockerSvc.config);
|
|
expect(composeConfig).to.not.deep.equal(dockerConfig);
|
|
|
|
expect(dockerSvc.isEqualConfig(composeSvc, { test: 'abcdef' })).to.be
|
|
.true;
|
|
});
|
|
|
|
it('should restart a container when its dependent network mode container changes', async () => {
|
|
const composeSvc = await Service.fromComposeObject(
|
|
configs.networkModeService.compose,
|
|
configs.networkModeService.imageInfo,
|
|
);
|
|
const dockerSvc = Service.fromDockerContainer(
|
|
configs.networkModeService.inspect,
|
|
);
|
|
|
|
const composeConfig = omitConfigForComparison(composeSvc.config);
|
|
const dockerConfig = omitConfigForComparison(dockerSvc.config);
|
|
expect(composeConfig).to.not.deep.equal(dockerConfig);
|
|
|
|
return expect(dockerSvc.isEqualConfig(composeSvc, { test: 'qwerty' })).to
|
|
.be.false;
|
|
});
|
|
});
|
|
|
|
describe('Security options', () => {
|
|
it('ignores selinux security options on the target state', async () => {
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId: 123,
|
|
serviceId: 123,
|
|
serviceName: 'test',
|
|
composition: {
|
|
securityOpt: [
|
|
'label=user:USER',
|
|
'label=user:ROLE',
|
|
'seccomp=unconfined',
|
|
],
|
|
},
|
|
},
|
|
{ appName: 'test' } as any,
|
|
);
|
|
|
|
expect(service.config)
|
|
.to.have.property('securityOpt')
|
|
.that.deep.equals(['seccomp=unconfined']);
|
|
});
|
|
|
|
it('ignores selinux security options on the current state', async () => {
|
|
const mockContainer = createContainer({
|
|
Id: 'deadbeef',
|
|
Name: 'main_431889_572579',
|
|
Config: {
|
|
Labels: {
|
|
'io.resin.app-id': '1011165',
|
|
'io.resin.architecture': 'armv7hf',
|
|
'io.resin.service-id': '43697',
|
|
'io.resin.service-name': 'main',
|
|
'io.resin.supervised': 'true',
|
|
},
|
|
},
|
|
HostConfig: {
|
|
SecurityOpt: ['label=disable', 'seccomp=unconfined'],
|
|
},
|
|
});
|
|
const service = Service.fromDockerContainer(
|
|
await mockContainer.inspect(),
|
|
);
|
|
|
|
expect(service.config)
|
|
.to.have.property('securityOpt')
|
|
.that.deep.equals(['seccomp=unconfined']);
|
|
});
|
|
});
|
|
|
|
describe('Long syntax volume configuration', () => {
|
|
it('should generate a docker container config from a compose object (compose -> Service, Service -> container)', async () => {
|
|
/**
|
|
* compose -> Service (fromComposeObject)
|
|
*/
|
|
const appId = 5;
|
|
const serviceName = 'main';
|
|
const longSyntaxVolumes = [
|
|
// Long named volume
|
|
{
|
|
type: 'volume',
|
|
source: 'another',
|
|
target: '/another',
|
|
readOnly: true,
|
|
},
|
|
// Long anonymous volume
|
|
{
|
|
type: 'volume',
|
|
target: '/yet/another',
|
|
},
|
|
{ type: 'bind', source: '/mnt/data', target: '/data' },
|
|
{ type: 'tmpfs', target: '/home/tmp' },
|
|
];
|
|
const service = await Service.fromComposeObject(
|
|
{
|
|
appId,
|
|
serviceName,
|
|
releaseId: 4,
|
|
serviceId: 3,
|
|
imageId: 2,
|
|
composition: {
|
|
volumes: [
|
|
'myvolume:/myvolume',
|
|
'readonly:/readonly:ro',
|
|
'/home/mybind:/mybind',
|
|
'anonymous_volume',
|
|
...longSyntaxVolumes,
|
|
],
|
|
tmpfs: ['/var/tmp'],
|
|
},
|
|
},
|
|
{ appName: 'bar' } as any,
|
|
);
|
|
|
|
// Only tmpfs from composition should be added to config.tmpfs
|
|
expect(service.config.tmpfs).to.deep.equal(['/var/tmp']);
|
|
// config.volumes should include all long syntax and short syntax mounts, excluding binds
|
|
expect(service.config.volumes).to.deep.include.members([
|
|
`${appId}_myvolume:/myvolume`,
|
|
`${appId}_readonly:/readonly:ro`,
|
|
'anonymous_volume',
|
|
...longSyntaxVolumes.filter(({ type }) => type !== 'bind'),
|
|
`/tmp/balena-supervisor/services/${appId}/${serviceName}:/tmp/resin`,
|
|
`/tmp/balena-supervisor/services/${appId}/${serviceName}:/tmp/balena`,
|
|
]);
|
|
// bind mounts are not allowed
|
|
expect(service.config.volumes).to.not.deep.include.members([
|
|
'/home/mybind:/mybind',
|
|
...longSyntaxVolumes.filter(({ type }) => type === 'bind'),
|
|
]);
|
|
|
|
/**
|
|
* Service -> container (toDockerContainer)
|
|
*/
|
|
// Inject bind mounts (as feature labels would)
|
|
// Bind mounts added under feature labels use the short syntax, except for the engine label
|
|
service.config.volumes.push('/var/log/journal:/var/log/journal:ro');
|
|
service.config.volumes.push({
|
|
type: 'bind',
|
|
source: constants.dockerSocket,
|
|
target: constants.containerDockerSocket,
|
|
} as ServiceT.LongBind);
|
|
|
|
const ctn = service.toDockerContainer({
|
|
deviceName: 'thicc_nucc',
|
|
} as any);
|
|
// Only long syntax volumes should be listed under HostConfig.Mounts.
|
|
// This includes any tmpfs volumes defined using long syntax, consistent
|
|
// with docker-compose's behavior.
|
|
expect(ctn.HostConfig)
|
|
.to.have.property('Mounts')
|
|
.that.deep.includes.members([
|
|
{
|
|
Type: 'volume',
|
|
Source: `${appId}_another`, // Should be namespaced by appId
|
|
Target: '/another',
|
|
ReadOnly: true,
|
|
},
|
|
{ Type: 'tmpfs', Target: '/home/tmp' },
|
|
{
|
|
Type: 'bind',
|
|
Source: constants.dockerSocket,
|
|
Target: constants.containerDockerSocket,
|
|
},
|
|
]);
|
|
|
|
// bind mounts except for the engine feature label should be filtered out
|
|
expect(ctn.HostConfig)
|
|
.to.have.property('Mounts')
|
|
.that.does.not.deep.include.members([
|
|
{ Type: 'bind', Source: '/mnt/data', Target: '/data' },
|
|
]);
|
|
|
|
// Short syntax volumes should be configured as HostConfig.Binds
|
|
expect(ctn.HostConfig)
|
|
.to.have.property('Binds')
|
|
.that.includes.members([
|
|
`${appId}_myvolume:/myvolume`,
|
|
`${appId}_readonly:/readonly:ro`,
|
|
`/tmp/balena-supervisor/services/${appId}/${serviceName}:/tmp/resin`,
|
|
`/tmp/balena-supervisor/services/${appId}/${serviceName}:/tmp/balena`,
|
|
'/var/log/journal:/var/log/journal:ro',
|
|
]);
|
|
|
|
// Tmpfs volumes defined through compose's service.tmpfs are under HostConfig.Tmpfs.
|
|
// Otherwise tmpfs volumes defined through compose's service.volumes as type: 'tmpfs' are under HostConfig.Mounts.
|
|
expect(ctn.HostConfig).to.have.property('Tmpfs').that.deep.equals({
|
|
'/var/tmp': '',
|
|
});
|
|
});
|
|
|
|
it('should generate a service instance from a docker container (container -> Service)', async () => {
|
|
const appId = 6;
|
|
const mockContainer = createContainer({
|
|
Id: 'deadbeef',
|
|
Name: 'main_123_456_789',
|
|
HostConfig: {
|
|
Binds: [
|
|
`${appId}_test:/test`,
|
|
`${appId}_test2:/test2:ro`,
|
|
'/proc:/proc',
|
|
'/etc/machine-id:/etc/machine-id:ro',
|
|
],
|
|
Tmpfs: {
|
|
'/var/tmp1': '',
|
|
},
|
|
Mounts: [
|
|
{
|
|
Type: 'volume',
|
|
Source: '6_test3',
|
|
Target: '/test3',
|
|
},
|
|
{
|
|
Type: 'tmpfs',
|
|
Target: '/var/tmp2',
|
|
} as any,
|
|
// Dockerode typings require a Source field but tmpfs doesn't require Source
|
|
],
|
|
},
|
|
Config: {
|
|
Volumes: {
|
|
'/var/lib/volume': {},
|
|
},
|
|
Labels: {
|
|
'io.balena.app-id': `${appId}`,
|
|
'io.balena.architecture': 'amd64',
|
|
'io.balena.service-id': '123',
|
|
'io.balena.service-name': 'main',
|
|
'io.balena.supervised': 'true',
|
|
},
|
|
},
|
|
});
|
|
|
|
const service = Service.fromDockerContainer(mockContainer.inspectInfo);
|
|
// service.volumes should combine:
|
|
// - HostConfig.Binds
|
|
// - 'volume'|'tmpfs' types from HostConfig.Mounts
|
|
// - Config.Volumes
|
|
expect(service.config)
|
|
.to.have.property('volumes')
|
|
.that.deep.includes.members([
|
|
`${appId}_test:/test`,
|
|
`${appId}_test2:/test2:ro`,
|
|
'/proc:/proc',
|
|
'/etc/machine-id:/etc/machine-id:ro',
|
|
'/var/lib/volume',
|
|
{
|
|
type: 'volume',
|
|
source: '6_test3',
|
|
target: '/test3',
|
|
},
|
|
{
|
|
type: 'tmpfs',
|
|
target: '/var/tmp2',
|
|
},
|
|
]);
|
|
|
|
// service.tmpfs should only include HostConfig.Tmpfs,
|
|
// 'tmpfs' types defined with long syntax belong to HostConfig.Mounts
|
|
// and therefore are added to service.config.volumes.
|
|
expect(service.config)
|
|
.to.have.property('tmpfs')
|
|
.that.deep.equals(['/var/tmp1']);
|
|
});
|
|
});
|
|
|
|
describe('Service volume types', () => {
|
|
it('should correctly identify short syntax volumes', () => {
|
|
// Short binds
|
|
['/one:/one', '/two:/two:ro', '/three:/three:rw'].forEach((b) => {
|
|
expect(ServiceT.ShortMount.is(b)).to.be.true;
|
|
expect(ServiceT.ShortBind.is(b)).to.be.true;
|
|
expect(ServiceT.ShortAnonymousVolume.is(b)).to.be.false;
|
|
expect(ServiceT.ShortNamedVolume.is(b)).to.be.false;
|
|
});
|
|
// Short anonymous volumes
|
|
['volume', 'another_volume'].forEach((v) => {
|
|
expect(ServiceT.ShortMount.is(v)).to.be.false;
|
|
expect(ServiceT.ShortBind.is(v)).to.be.false;
|
|
expect(ServiceT.ShortAnonymousVolume.is(v)).to.be.true;
|
|
expect(ServiceT.ShortNamedVolume.is(v)).to.be.false;
|
|
});
|
|
// Short named volumes
|
|
[
|
|
'another_one:/another/one',
|
|
'yet_another:/yet/another:ro',
|
|
'final:/final:rw',
|
|
].forEach((v) => {
|
|
expect(ServiceT.ShortMount.is(v)).to.be.true;
|
|
expect(ServiceT.ShortBind.is(v)).to.be.false;
|
|
expect(ServiceT.ShortAnonymousVolume.is(v)).to.be.false;
|
|
expect(ServiceT.ShortNamedVolume.is(v)).to.be.true;
|
|
});
|
|
});
|
|
|
|
it('should correctly identify long syntax volumes', () => {
|
|
// For all the following examples where optionals are defined, only the option with key equal to the type
|
|
// will be applied to the resulting volume, but the other options shouldn't cause the type check to return false.
|
|
// For example, for a definition with { type: volume }, only { volume: { nocopy: boolean }} option will apply.
|
|
const longAnonymousVols = [
|
|
{ type: 'volume', target: '/one' },
|
|
{ type: 'volume', target: '/two', readOnly: true },
|
|
{ type: 'volume', target: '/three', volume: { nocopy: true } },
|
|
{ type: 'volume', target: '/four', bind: { propagation: 'slave' } },
|
|
{ type: 'volume', target: '/five', tmpfs: { size: 200 } },
|
|
];
|
|
longAnonymousVols.forEach((v) => {
|
|
expect(ServiceT.LongAnonymousVolume.is(v)).to.be.true;
|
|
expect(ServiceT.LongNamedVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongBind.is(v)).to.be.false;
|
|
expect(ServiceT.LongTmpfs.is(v)).to.be.false;
|
|
});
|
|
|
|
const longNamedVols = [
|
|
{ type: 'volume', source: 'one', target: '/one' },
|
|
{ type: 'volume', source: 'two', target: '/two', readOnly: false },
|
|
{
|
|
type: 'volume',
|
|
source: 'three',
|
|
target: '/three',
|
|
volume: { nocopy: false },
|
|
},
|
|
{
|
|
type: 'volume',
|
|
source: 'four',
|
|
target: '/four',
|
|
bind: { propagation: 'slave' },
|
|
},
|
|
{
|
|
type: 'volume',
|
|
source: 'five',
|
|
target: '/five',
|
|
tmpfs: { size: 200 },
|
|
},
|
|
];
|
|
longNamedVols.forEach((v) => {
|
|
expect(ServiceT.LongAnonymousVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongNamedVolume.is(v)).to.be.true;
|
|
expect(ServiceT.LongBind.is(v)).to.be.false;
|
|
expect(ServiceT.LongTmpfs.is(v)).to.be.false;
|
|
});
|
|
|
|
const longBinds = [
|
|
{ type: 'bind', source: '/one', target: '/one' },
|
|
{ type: 'bind', source: '/two', target: '/two', readOnly: true },
|
|
{
|
|
type: 'bind',
|
|
source: '/three',
|
|
target: '/three',
|
|
volume: { nocopy: false },
|
|
},
|
|
{
|
|
type: 'bind',
|
|
source: '/four',
|
|
target: '/four',
|
|
bind: { propagation: 'slave' },
|
|
},
|
|
{
|
|
type: 'bind',
|
|
source: '/five',
|
|
target: '/five',
|
|
tmpfs: { size: 200 },
|
|
},
|
|
];
|
|
longBinds.forEach((v) => {
|
|
expect(ServiceT.LongAnonymousVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongNamedVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongBind.is(v)).to.be.true;
|
|
expect(ServiceT.LongTmpfs.is(v)).to.be.false;
|
|
});
|
|
|
|
const longTmpfs = [
|
|
{ type: 'tmpfs', target: '/var/tmp' },
|
|
{ type: 'tmpfs', target: '/var/tmp2', readOnly: false },
|
|
{ type: 'tmpfs', target: '/var/tmp3', volume: { nocopy: false } },
|
|
{ type: 'tmpfs', target: '/var/tmp4', bind: { propagation: 'slave' } },
|
|
{ type: 'tmpfs', target: '/var/tmp4', tmpfs: { size: 200 } },
|
|
];
|
|
longTmpfs.forEach((v) => {
|
|
expect(ServiceT.LongAnonymousVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongNamedVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongBind.is(v)).to.be.false;
|
|
expect(ServiceT.LongTmpfs.is(v)).to.be.true;
|
|
});
|
|
|
|
// All of the following volume definitions are not allowed by docker-compose
|
|
const invalids = [
|
|
// bind without source
|
|
{ type: 'bind', target: '/test' },
|
|
// bind with source that's not an absolute path
|
|
{ type: 'bind', source: 'not_a_bind', target: '/bind' },
|
|
// tmpfs with source
|
|
{ type: 'tmpfs', source: '/var/tmp', target: '/home/tmp' },
|
|
// Other types besides volume, tmpfs, or bind
|
|
{ type: 'invalid', source: 'test', target: '/test2' },
|
|
];
|
|
invalids.forEach((v) => {
|
|
expect(ServiceT.LongAnonymousVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongNamedVolume.is(v)).to.be.false;
|
|
expect(ServiceT.LongBind.is(v)).to.be.false;
|
|
expect(ServiceT.LongTmpfs.is(v)).to.be.false;
|
|
});
|
|
});
|
|
});
|
|
});
|