import * as _ from 'lodash'; import * as sinon from 'sinon'; import { expect } from 'chai'; import { createContainer } from '../../lib/mockerode'; import Service from '../../../src/compose/service'; import Volume from '../../../src/compose/volume'; import { ServiceComposeConfig, ServiceConfig, } from '../../../src/compose/types/service'; import * as constants from '../../../src/lib/constants'; import * as apiKeys from '../../../src/lib/api-keys'; import log from '../../../src/lib/supervisor-console'; const configs = { simple: { compose: require('../../data/docker-states/simple/compose.json'), imageInfo: require('../../data/docker-states/simple/imageInfo.json'), inspect: require('../../data/docker-states/simple/inspect.json'), }, entrypoint: { compose: require('../../data/docker-states/entrypoint/compose.json'), imageInfo: require('../../data/docker-states/entrypoint/imageInfo.json'), inspect: require('../../data/docker-states/entrypoint/inspect.json'), }, networkModeService: { compose: require('../../data/docker-states/network-mode-service/compose.json'), imageInfo: require('../../data/docker-states/network-mode-service/imageInfo.json'), inspect: require('../../data/docker-states/network-mode-service/inspect.json'), }, }; describe('compose/service', () => { before(() => { // disable log output during testing sinon.stub(log, 'debug'); sinon.stub(log, 'warn'); sinon.stub(log, 'info'); sinon.stub(log, 'success'); }); after(() => { sinon.restore(); }); 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: ServiceComposeConfig['networks'], ) => await Service.fromComposeObject( { appId: 123456, 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: { '123456_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: { '123456_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 an 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.include.members([ `${constants.dockerSocket}:${constants.containerDockerSocket}`, ]); 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('io.balena.supervisor-api', () => { it('sets BALENA_SUPERVISOR_HOST, BALENA_SUPERVISOR_PORT and BALENA_SUPERVISOR_ADDRESS env vars', async () => { const service = await Service.fromComposeObject( { appId: 123456, serviceId: 123456, serviceName: 'foobar', labels: { 'io.balena.features.supervisor-api': '1', }, }, { appName: 'test', supervisorApiHost: 'supervisor', listenPort: 48484, } as any, ); expect( service.config.environment['BALENA_SUPERVISOR_HOST'], ).to.be.equal('supervisor'); expect( service.config.environment['BALENA_SUPERVISOR_PORT'], ).to.be.equal('48484'); expect( service.config.environment['BALENA_SUPERVISOR_ADDRESS'], ).to.be.equal('http://supervisor:48484'); }); it('sets BALENA_API_KEY env var to the scoped API key value', async () => { // TODO: should we add an integration test that checks that the value used for the API key comes // from the database sinon.stub(apiKeys, 'generateScopedKey').resolves('this is a secret'); const service = await Service.fromComposeObject( { appId: 123456, serviceId: 123456, serviceName: 'foobar', labels: { 'io.balena.features.supervisor-api': '1', }, }, { appName: 'test', supervisorApiHost: 'supervisor', listenPort: 48484, } as any, ); expect( service.config.environment['BALENA_SUPERVISOR_API_KEY'], ).to.be.equal('this is a secret'); (apiKeys.generateScopedKey as sinon.SinonStub).restore(); }); }); }); describe('Creating service instances from docker configuration', () => { const omitConfigForComparison = (config: 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'], }, }).config.networks, ).to.deep.equal({ '123456_balena': { ipv4Address: '1.2.3.4', ipv6Address: '5.6.7.8', linkLocalIps: ['123.123.123'], aliases: ['test', '1123'], }, }); }); }); }); describe('Network mode service:', () => { const omitConfigForComparison = (config: 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']); }); }); });