2021-05-05 20:32:31 +00:00
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' ,
2021-08-03 23:12:47 +00:00
appUuid : 'deadbeef' ,
2021-05-05 20:32:31 +00:00
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' ,
2021-08-03 23:12:47 +00:00
RESIN_APP_UUID : 'deadbeef' ,
2021-05-05 20:32:31 +00:00
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' ,
2021-08-03 23:12:47 +00:00
BALENA_APP_UUID : 'deadbeef' ,
2021-05-05 20:32:31 +00:00
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 ,
2021-08-03 23:12:47 +00:00
composition : {
expose : [ 1000 , '243/udp' ] ,
ports : [ '2344' , '2345:2354' , '2346:2367/udp' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{
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 ,
2021-08-03 23:12:47 +00:00
composition : {
expose : [ 1000 , '243/udp' ] ,
ports : [ '1000-1003:2000-2003' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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 ,
2021-08-03 23:12:47 +00:00
composition : {
ports : [ '5-65536:5-65536/tcp' , '5-65536:5-65536/udp' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
ports : [ '80:80' , '100:100' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [
'vol1:vol2' ,
'vol3 :/usr/src/app' ,
'vol4: /usr/src/app' ,
'vol5 : vol6' ,
] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
mem_limit : memLimit ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
workingDir : workdir ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
networks ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
dns : [ '8.8.8.8' , '1.1.1.1' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ appName : 'test' } as any ,
) ;
let svc2 = await Service . fromComposeObject (
{
appId : 1 ,
serviceId : 1 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
dns : [ '8.8.8.8' , '1.1.1.1' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ appName : 'test' } as any ,
) ;
expect ( svc1 . isEqualConfig ( svc2 , { } ) ) . to . be . true ;
svc2 = await Service . fromComposeObject (
{
appId : 1 ,
serviceId : 1 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
dns : [ '1.1.1.1' , '8.8.8.8' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [ 'abcdef' , 'ghijk' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ appName : 'test' } as any ,
) ;
let svc2 = await Service . fromComposeObject (
{
appId : 1 ,
serviceId : 1 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [ 'abcdef' , 'ghijk' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ appName : 'test' } as any ,
) ;
expect ( svc1 . isEqualConfig ( svc2 , { } ) ) . to . be . true ;
svc2 = await Service . fromComposeObject (
{
appId : 1 ,
serviceId : 1 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [ 'ghijk' , 'abcdef' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [ 'abcdef' , 'ghijk' ] ,
dns : [ '8.8.8.8' , '1.1.1.1' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ appName : 'test' } as any ,
) ;
const svc2 = await Service . fromComposeObject (
{
appId : 1 ,
serviceId : 1 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
volumes : [ 'ghijk' , 'abcdef' ] ,
dns : [ '8.8.8.8' , '1.1.1.1' ] ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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 : {
module s : 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 : {
module s : 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 ,
2021-08-03 23:12:47 +00:00
composition : {
network_mode : 'service: test' ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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 ,
2021-08-03 23:12:47 +00:00
composition : {
depends_on : [ 'another_service' ] ,
network_mode : 'service: test' ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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 ,
2021-08-03 23:12:47 +00:00
composition : {
network_mode : 'service: test' ,
} ,
2021-05-05 20:32:31 +00:00
} ,
{ 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 ;
} ) ;
} ) ;
2022-02-23 21:03:44 +00:00
describe ( 'Security options' , ( ) = > {
it ( 'ignores selinux security options on the target state' , async ( ) = > {
const service = await Service . fromComposeObject (
{
appId : 123 ,
serviceId : 123 ,
serviceName : 'test' ,
2021-08-03 23:12:47 +00:00
composition : {
securityOpt : [
'label=user:USER' ,
'label=user:ROLE' ,
'seccomp=unconfined' ,
] ,
} ,
2022-02-23 21:03:44 +00:00
} ,
{ 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' ] ) ;
} ) ;
} ) ;
2021-05-05 20:32:31 +00:00
} ) ;