From ed1c18e3691e60bc22652e930be49e428d2ef1fe Mon Sep 17 00:00:00 2001 From: Christina Ying Wang Date: Mon, 9 Sep 2024 14:39:42 -0700 Subject: [PATCH] Add support for init field from compose Init supports boolean values, and is not included in the config when not defined. Change-type: patch Signed-off-by: Christina Ying Wang --- src/compose/service.ts | 13 + src/compose/types/service.ts | 3 +- .../docker-states/entrypoint/inspect.json | 3 +- test/data/docker-states/init/compose.json | 17 ++ test/data/docker-states/init/imageInfo.json | 103 ++++++++ test/data/docker-states/init/inspect.json | 232 ++++++++++++++++++ test/unit/compose/service.spec.ts | 44 ++++ 7 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 test/data/docker-states/init/compose.json create mode 100644 test/data/docker-states/init/imageInfo.json create mode 100644 test/data/docker-states/init/inspect.json diff --git a/src/compose/service.ts b/src/compose/service.ts index 7bd454d4f..e0f495465 100644 --- a/src/compose/service.ts +++ b/src/compose/service.ts @@ -337,6 +337,13 @@ class ServiceImpl implements Service { config.tty = Boolean(config.tty); } + // Only keep init field if it's a boolean + if (config.init != null) { + config.init = Boolean(config.init); + } else { + delete config.init; + } + if (Array.isArray(config.sysctls)) { config.sysctls = _.fromPairs( _.map(config.sysctls, (v) => _.split(v, '=')), @@ -598,6 +605,11 @@ class ServiceImpl implements Service { tty: container.Config.Tty || false, }; + // Only add `init` if true or false, otherwise leave blank + if (typeof container.HostConfig.Init === 'boolean') { + svc.config.init = container.HostConfig.Init; + } + const appId = checkInt(svc.config.labels['io.balena.app-id']); if (appId == null) { throw new InternalInconsistencyError( @@ -739,6 +751,7 @@ class ServiceImpl implements Service { UsernsMode: this.config.usernsMode, NanoCpus: this.config.cpus, IpcMode: this.config.ipc, + Init: this.config.init, } as Dockerode.ContainerCreateOptions['HostConfig'], Healthcheck: ComposeUtils.serviceHealthcheckToDockerHealthcheck( this.config.healthcheck, diff --git a/src/compose/types/service.ts b/src/compose/types/service.ts index 7dd11fa38..9add94885 100644 --- a/src/compose/types/service.ts +++ b/src/compose/types/service.ts @@ -183,7 +183,7 @@ export interface ServiceComposeConfig { groupAdd?: string[]; healthcheck?: ComposeHealthcheck; image: string; - init?: string | boolean; + init?: boolean; labels?: { [labelName: string]: string }; running?: boolean; networkMode?: string; @@ -272,6 +272,7 @@ export interface ServiceConfig { domainname: string; hostname: string; ipc: string; + init?: boolean; macAddress: string; memLimit: number; memReservation: number; diff --git a/test/data/docker-states/entrypoint/inspect.json b/test/data/docker-states/entrypoint/inspect.json index 9699d7f3d..fa8bf83cc 100644 --- a/test/data/docker-states/entrypoint/inspect.json +++ b/test/data/docker-states/entrypoint/inspect.json @@ -105,8 +105,7 @@ "CpuCount": 0, "CpuPercent": 0, "IOMaximumIOps": 0, - "IOMaximumBandwidth": 0, - "Init": false + "IOMaximumBandwidth": 0 }, "GraphDriver": { "Data": null, diff --git a/test/data/docker-states/init/compose.json b/test/data/docker-states/init/compose.json new file mode 100644 index 000000000..dbfadd218 --- /dev/null +++ b/test/data/docker-states/init/compose.json @@ -0,0 +1,17 @@ +{ + "imageId": 478890, + "serviceName": "main", + "image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b", + "running": true, + "environment": {}, + "labels": {}, + "appId": 1011165, + "appUuid": "aaaaaaaa", + "releaseId": 597007, + "serviceId": 43697, + "commit": "ff300a701054ac15281de1f9c0e84b8c", + "imageName": "registry2.resin.io/v2/bf9c649a5ac2fe147bbe350875042388@sha256:3a5c17b715b4f8265539c1a006dd1abdd2ff3b758aa23df99f77c792f40c3d43", + "composition": { + "init": true + } +} diff --git a/test/data/docker-states/init/imageInfo.json b/test/data/docker-states/init/imageInfo.json new file mode 100644 index 000000000..e84d60bd1 --- /dev/null +++ b/test/data/docker-states/init/imageInfo.json @@ -0,0 +1,103 @@ +{ + "serviceName": "main", + "imageInfo": { + "Id": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b", + "RepoTags": [], + "RepoDigests": [ + "registry2.resin.io/v2/90e3bf4c3dc1e59221b7b3e659a327f6@sha256:3a5c17b715b4f8265539c1a006dd1abdd2ff3b758aa23df99f77c792f40c3d43" + ], + "Parent": "", + "Comment": "", + "Created": "2018-09-12T13:00:43.974720835Z", + "Container": "07cb0400e218ae235e67cf1fd283dc09559f57fbe2a36f5cc89302388371781c", + "ContainerConfig": { + "Hostname": "137f767087a2", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "CMD [\"/bin/sh\" \"-c\" \"while true; do echo 'hello'; sleep 5; done;\"]" + ], + "ArgsEscaped": true, + "Image": "sha256:8d68949dbddcb3ab1a61caeffa0aa1a6e27425ecc4f7665d04d8d0e5bfa03298", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": [], + "Labels": {} + }, + "DockerVersion": "17.05.0-ce", + "Author": "", + "Config": { + "Hostname": "137f767087a2", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "while true; do echo 'hello'; sleep 5; done;" + ], + "ArgsEscaped": true, + "Image": "sha256:8d68949dbddcb3ab1a61caeffa0aa1a6e27425ecc4f7665d04d8d0e5bfa03298", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": [], + "Labels": {} + }, + "Architecture": "arm64", + "Os": "linux", + "Size": 104966431, + "VirtualSize": 104966431, + "GraphDriver": { + "Data": null, + "Name": "aufs" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:a3075e9def48840598abcfe08c1ee564c989d1014d847082d950dca2c94098ec", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } + }, + "appName": "supervisortest", + "supervisorApiHost": "172.17.0.1", + "hostPathExists": { + "firmware": true, + "modules": true + }, + "hostname": "7dadabd", + "uuid": "a7feb967fac7f559ccf2a006a36bcf5d", + "listenPort": "48484", + "name": "Office", + "apiSecret": "d4bf8369519c32adaa5dd1f84367aa817403f2a3ce976be9c9bacd4d344fdd", + "deviceApiKey": "ff89e1d8db58a7ca52a435f2adea319a", + "version": "7.18.0", + "deviceArch": "amd64", + "deviceType": "raspberrypi3", + "osVersion": "Resin OS 2.13.6+rev1" +} diff --git a/test/data/docker-states/init/inspect.json b/test/data/docker-states/init/inspect.json new file mode 100644 index 000000000..9105ede1c --- /dev/null +++ b/test/data/docker-states/init/inspect.json @@ -0,0 +1,232 @@ +{ + "Id": "52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e", + "Created": "2018-09-12T14:38:42.696028995Z", + "Path": "/bin/sh", + "Args": [ + "-c", + "while true; do echo 'hello'; sleep 5; done;" + ], + "State": { + "Status": "exited", + "Running": false, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 0, + "ExitCode": 137, + "Error": "", + "StartedAt": "2018-09-12T14:38:45.408574694Z", + "FinishedAt": "2018-09-12T14:38:46.462783621Z" + }, + "Image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b", + "ResolvConfPath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/hostname", + "HostsPath": "/var/lib/docker/containers/52cfd7a64d50236376741dd2578c4fbb0178d90e2e4fae55f3e14cd905e9ac9e/hosts", + "LogPath": "", + "Name": "main_1_1", + "RestartCount": 0, + "Driver": "aufs", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": [ + "/tmp/balena-supervisor/services/1011165/main:/tmp/resin", + "/tmp/balena-supervisor/services/1011165/main:/tmp/balena" + ], + "ContainerIDFile": "", + "LogConfig": { + "Type": "journald", + "Config": {} + }, + "NetworkMode": "aaaaaaaa_default", + "PortBindings": {}, + "RestartPolicy": { + "Name": "always", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "CapAdd": [], + "CapDrop": [], + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": [], + "GroupAdd": [], + "IpcMode": "shareable", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": [], + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "ConsoleSize": [ + 0, + 0 + ], + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DiskQuota": 0, + "KernelMemory": 0, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": -1, + "OomKillDisable": false, + "PidsLimit": 0, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "Init": true + }, + "GraphDriver": { + "Data": null, + "Name": "aufs" + }, + "Mounts": [ + { + "Type": "bind", + "Source": "/tmp/balena-supervisor/services/1011165/main", + "Destination": "/tmp/resin", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + }, + { + "Type": "bind", + "Source": "/tmp/balena-supervisor/services/1011165/main", + "Destination": "/tmp/balena", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + } + ], + "Config": { + "Hostname": "52cfd7a64d50", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": true, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "RESIN_APP_ID=1011165", + "RESIN_APP_UUID=aaaaaaaa", + "RESIN_APP_NAME=supervisortest", + "RESIN_SERVICE_NAME=main", + "RESIN_DEVICE_UUID=a7feb967fac7f559ccf2a006a36bcf5d", + "RESIN_DEVICE_ARCH=amd64", + "RESIN_DEVICE_TYPE=raspberrypi3", + "RESIN_HOST_OS_VERSION=Resin OS 2.13.6+rev1", + "RESIN_APP_LOCK_PATH=/tmp/balena/updates.lock", + "RESIN_SERVICE_KILL_ME_PATH=/tmp/balena/handover-complete", + "RESIN=1", + "BALENA_APP_ID=1011165", + "BALENA_APP_UUID=aaaaaaaa", + "BALENA_APP_NAME=supervisortest", + "BALENA_SERVICE_NAME=main", + "BALENA_DEVICE_UUID=a7feb967fac7f559ccf2a006a36bcf5d", + "BALENA_DEVICE_ARCH=amd64", + "BALENA_DEVICE_TYPE=raspberrypi3", + "BALENA_HOST_OS_VERSION=Resin OS 2.13.6+rev1", + "BALENA_APP_LOCK_PATH=/tmp/balena/updates.lock", + "BALENA_SERVICE_HANDOVER_COMPLETE_PATH=/tmp/balena/handover-complete", + "BALENA=1", + "USER=root", + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "while true; do echo 'hello'; sleep 5; done;" + ], + "Healthcheck": { + "Test": [ + "NONE" + ] + }, + "Image": "sha256:7f54fa690ce19a1f625b04479ae1f12f44d36112a74be7edfefa777ecfdb194b", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.resin.app-id": "1011165", + "io.balena.app-uuid": "aaaaaaaa", + "io.resin.service-id": "43697", + "io.resin.service-name": "main", + "io.resin.supervised": "true" + }, + "StopTimeout": 0 + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "bf4952b7f6695a8f05da1807946723b37e1041b8f41588678d6dece310270990", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": {}, + "SandboxKey": "/var/run/balena/netns/bf4952b7f669", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "aaaaaaaa_default": { + "IPAMConfig": {}, + "Links": null, + "Aliases": [ + "main", + "52cfd7a64d50" + ], + "NetworkID": "f88716ed3d340f1b9aa61df22d92ce6ad8aa752d8bf8e4aa6e74142dea677465", + "EndpointID": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "", + "DriverOpts": null + } + } + } +} diff --git a/test/unit/compose/service.spec.ts b/test/unit/compose/service.spec.ts index 1c7d262e0..7efffc3d0 100644 --- a/test/unit/compose/service.spec.ts +++ b/test/unit/compose/service.spec.ts @@ -24,6 +24,11 @@ const configs = { imageInfo: require('~/test-data/docker-states/network-mode-service/imageInfo.json'), inspect: require('~/test-data/docker-states/network-mode-service/inspect.json'), }, + init: { + compose: require('~/test-data/docker-states/init/compose.json'), + imageInfo: require('~/test-data/docker-states/init/imageInfo.json'), + inspect: require('~/test-data/docker-states/init/inspect.json'), + }, }; describe('compose/service: unit tests', () => { @@ -263,6 +268,31 @@ describe('compose/service: unit tests', () => { ]); }); + it('should support init property', async () => { + const appConfigWithInit = (init?: boolean) => ({ + appId: 123, + serviceId: 123, + serviceName: 'test', + composition: { + init, + }, + }); + const svc = await Service.fromComposeObject(appConfigWithInit(true), { + appName: 'test', + } as any); + expect(svc.config).to.have.property('init').that.equals(true); + + const svc2 = await Service.fromComposeObject(appConfigWithInit(false), { + appName: 'test', + } as any); + expect(svc2.config).to.have.property('init').that.equals(false); + + const svc3 = await Service.fromComposeObject(appConfigWithInit(), { + appName: 'test', + } as any); + expect(svc3.config).to.not.have.property('init'); + }); + describe('Parsing memory strings from compose configuration', () => { const makeComposeServiceWithLimit = async (memLimit?: string | number) => await Service.fromComposeObject( @@ -898,6 +928,20 @@ describe('compose/service: unit tests', () => { expect(dockerSvc.isEqualConfig(composeSvc, {})).to.equals(true); }); + it('should correctly handle the init config', async () => { + const composeSvc = await Service.fromComposeObject( + configs.init.compose, + configs.init.imageInfo, + ); + const dockerSvc = Service.fromDockerContainer(configs.init.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({