528 lines
20 KiB
TypeScript
528 lines
20 KiB
TypeScript
/**
|
|
* Unit tests for inspectToCompose - the container-inspect -> compose YAML mapper (#489).
|
|
*
|
|
* Uses REAL `docker inspect` payloads captured from running containers
|
|
* (tests/fixtures/inspect/*.json), so the mapper is exercised against the exact shapes
|
|
* Docker actually emits, not hand-written approximations. Assertions run on the re-parsed
|
|
* YAML object so they are independent of formatting.
|
|
*
|
|
* Run with: bun test tests/inspect-to-compose.test.ts
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import yaml from 'js-yaml';
|
|
import { inspectToCompose, inspectToComposeService, type DockerInspect } from '../src/lib/utils/inspect-to-compose';
|
|
|
|
function loadFixture(name: string): DockerInspect {
|
|
const path = join(import.meta.dir, 'fixtures', 'inspect', name);
|
|
const arr = JSON.parse(readFileSync(path, 'utf8')) as DockerInspect[];
|
|
return arr[0];
|
|
}
|
|
|
|
function parseCompose(inspect: DockerInspect, options = {}) {
|
|
const yamlStr = inspectToCompose(inspect, options);
|
|
return { yamlStr, doc: yaml.load(yamlStr) as any };
|
|
}
|
|
|
|
describe('inspectToCompose - real gitea inspect (compose-managed, named-vol, custom net)', () => {
|
|
const gitea = loadFixture('gitea.json');
|
|
|
|
test('service key is the clean container name', () => {
|
|
const { doc } = parseCompose(gitea);
|
|
expect(Object.keys(doc.services)).toEqual(['semver-demo-stack-gitea-1']);
|
|
});
|
|
|
|
test('image + container_name + restart carry through', () => {
|
|
const { doc } = parseCompose(gitea);
|
|
const svc = doc.services['semver-demo-stack-gitea-1'];
|
|
expect(svc.image).toBe('gitea/gitea:1.21.0');
|
|
expect(svc.container_name).toBe('semver-demo-stack-gitea-1');
|
|
expect(svc.restart).toBe('unless-stopped');
|
|
});
|
|
|
|
test('the com.docker.compose.* labels are stripped', () => {
|
|
const { yamlStr, doc } = parseCompose(gitea);
|
|
expect(yamlStr).not.toContain('com.docker.compose');
|
|
const svc = doc.services['semver-demo-stack-gitea-1'];
|
|
if (svc.labels) {
|
|
for (const key of Object.keys(svc.labels)) {
|
|
expect(key.startsWith('com.docker.compose.')).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the custom stack network is emitted and declared external at top level', () => {
|
|
const { doc } = parseCompose(gitea);
|
|
const svc = doc.services['semver-demo-stack-gitea-1'];
|
|
// The network carries a per-container MAC, so it renders in the map form. Its auto
|
|
// aliases (the container name + the compose service name) are stripped, leaving no
|
|
// user aliases - only the mac_address.
|
|
expect(svc.networks).toEqual({
|
|
'semver-demo-stack_default': { mac_address: 'f6:1f:4a:64:b6:74' }
|
|
});
|
|
expect(doc.networks['semver-demo-stack_default']).toEqual({ external: true });
|
|
// bare bridge/default are never emitted
|
|
expect(svc.network_mode).toBeUndefined();
|
|
});
|
|
|
|
test('the anonymous (64-hex) volume becomes a bare anonymous mount, not a named one', () => {
|
|
const { doc } = parseCompose(gitea);
|
|
const svc = doc.services['semver-demo-stack-gitea-1'];
|
|
// gitea's /data rides a 64-hex anonymous volume -> `- /data`, no hash name, no top-level volume
|
|
expect(svc.volumes).toContain('/data');
|
|
for (const v of svc.volumes as string[]) {
|
|
expect(v).not.toMatch(/^[0-9a-f]{64}:/);
|
|
}
|
|
expect(doc.volumes).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - real nginx inspect (published ports, bind :ro, named vol, healthcheck, caps, sysctl)', () => {
|
|
const nginx = loadFixture('nginx-rich.json');
|
|
const svcName = 'itc-fixture';
|
|
|
|
test('published ports map to host:container, respecting a bound host IP', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const svc = doc.services[svcName];
|
|
expect(svc.ports).toContain('8099:80');
|
|
expect(svc.ports).toContain('127.0.0.1:8443:443');
|
|
});
|
|
|
|
test('bind mount is source:dest with :ro; named volume is name:dest + top-level external', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const svc = doc.services[svcName];
|
|
expect(svc.volumes).toContain('/tmp/itc-html:/usr/share/nginx/html:ro');
|
|
expect(svc.volumes).toContain('itc-named:/var/cache/nginx');
|
|
expect(doc.volumes['itc-named']).toEqual({ external: true });
|
|
});
|
|
|
|
test('restart on-failure keeps its retry count', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
expect(doc.services[svcName].restart).toBe('on-failure:5');
|
|
});
|
|
|
|
test('cap_add/cap_drop and sysctls carry through', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const svc = doc.services[svcName];
|
|
expect(svc.cap_add).toContain('CAP_NET_ADMIN');
|
|
expect(svc.cap_drop).toContain('CAP_MKNOD');
|
|
expect(svc.sysctls['net.core.somaxconn']).toBe('1024');
|
|
});
|
|
|
|
test('healthcheck maps Test + interval(ns->s) + retries', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const hc = doc.services[svcName].healthcheck;
|
|
expect(hc.test).toEqual(['CMD-SHELL', 'curl -f http://localhost/ || exit 1']);
|
|
expect(hc.interval).toBe('30s');
|
|
expect(hc.retries).toBe(3);
|
|
});
|
|
|
|
test('custom label survives', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
expect(doc.services[svcName].labels['my.custom.label']).toBe('hello');
|
|
});
|
|
|
|
test('image-baked labels (maintainer) are dropped when imageLabels is provided; user label stays', () => {
|
|
// The image sets maintainer; the container adds my.custom.label. With the image labels
|
|
// passed, only the user-added one survives.
|
|
const imageLabels = { maintainer: 'NGINX Docker Maintainers <docker-maint@nginx.com>' };
|
|
const { doc } = parseCompose(nginx, { imageLabels });
|
|
const labels = doc.services[svcName].labels;
|
|
expect(labels.maintainer).toBeUndefined();
|
|
expect(labels['my.custom.label']).toBe('hello');
|
|
});
|
|
|
|
test('auto-generated noise (hostname, working_dir /) is not emitted', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const svc = doc.services[svcName];
|
|
// Hostname is the short container id (12-hex) -> dropped; working_dir is "/" -> dropped.
|
|
expect(svc.hostname).toBeUndefined();
|
|
expect(svc.working_dir).toBeUndefined();
|
|
});
|
|
|
|
test('a non-default shm_size is emitted (this fixture set --shm-size)', () => {
|
|
const svc = parseCompose(nginx).doc.services[svcName];
|
|
expect(svc.shm_size).toBe(8399093760);
|
|
});
|
|
|
|
test('image-default entrypoint/command are dropped when the image values are provided', () => {
|
|
const imageEntrypoint = nginx.Config?.Entrypoint ?? null;
|
|
const imageCmd = nginx.Config?.Cmd ?? null;
|
|
const { doc } = parseCompose(nginx, { imageEntrypoint, imageCmd });
|
|
const svc = doc.services[svcName];
|
|
// nginx's entrypoint+cmd come from the image, so with the image passed they vanish.
|
|
expect(svc.entrypoint).toBeUndefined();
|
|
expect(svc.command).toBeUndefined();
|
|
});
|
|
|
|
test('a user-overridden command is KEPT even when the image command is provided', () => {
|
|
const custom = structuredClone(nginx);
|
|
custom.Config!.Cmd = ['nginx', '-g', 'daemon off; worker_processes 4;'];
|
|
const { doc } = parseCompose(custom, { imageCmd: nginx.Config?.Cmd ?? null });
|
|
expect(doc.services[svcName].command).toEqual(['nginx', '-g', 'daemon off; worker_processes 4;']);
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - image-env subtraction', () => {
|
|
const nginx = loadFixture('nginx-rich.json');
|
|
const svcName = 'itc-fixture';
|
|
|
|
test('without imageEnv, all env (incl. inherited PATH) is kept', () => {
|
|
const { doc } = parseCompose(nginx);
|
|
const env = (doc.services[svcName].environment as string[]) ?? [];
|
|
expect(env.some((e) => e.startsWith('PATH='))).toBe(true);
|
|
expect(env).toContain('MY_VAR=uservalue');
|
|
});
|
|
|
|
test('with imageEnv, inherited vars are dropped and only user-set env remains', () => {
|
|
const imageEnv = (nginx.Config?.Env ?? []).filter((e) => !e.startsWith('MY_VAR='));
|
|
const { doc } = parseCompose(nginx, { imageEnv });
|
|
const env = (doc.services[svcName].environment as string[]) ?? [];
|
|
expect(env).toEqual(['MY_VAR=uservalue']);
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - default-noise filtering', () => {
|
|
test('an explicit restart "no" and a bare bridge network produce no restart/network keys', () => {
|
|
const minimal: DockerInspect = {
|
|
Name: '/plain',
|
|
Config: { Image: 'busybox:latest' },
|
|
HostConfig: {
|
|
RestartPolicy: { Name: 'no', MaximumRetryCount: 0 },
|
|
NetworkMode: 'bridge',
|
|
LogConfig: { Type: 'json-file', Config: {} }
|
|
},
|
|
NetworkSettings: { Networks: { bridge: {} } }
|
|
};
|
|
const { doc } = parseCompose(minimal);
|
|
const svc = doc.services['plain'];
|
|
expect(svc.restart).toBeUndefined();
|
|
expect(svc.networks).toBeUndefined();
|
|
expect(svc.network_mode).toBeUndefined();
|
|
expect(svc.logging).toBeUndefined();
|
|
expect(svc.image).toBe('busybox:latest');
|
|
});
|
|
|
|
test('network_mode: host is emitted (a host-mode container must not fall back to bridge)', () => {
|
|
// #1464: a host-mode container reports NetworkMode "host" AND a `host` entry in
|
|
// NetworkSettings.Networks. The Networks entry is filtered (we never want
|
|
// `networks: [host]`), so the mode must be emitted from the else branch.
|
|
const inspect: DockerInspect = {
|
|
Name: '/hostnet',
|
|
Config: { Image: 'busybox:latest' },
|
|
HostConfig: { NetworkMode: 'host' },
|
|
NetworkSettings: { Networks: { host: {} } }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['hostnet'];
|
|
expect(svc.network_mode).toBe('host');
|
|
expect(svc.networks).toBeUndefined();
|
|
});
|
|
|
|
test('network_mode: none is emitted (disabled networking is a deliberate choice)', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/nonet',
|
|
Config: { Image: 'busybox:latest' },
|
|
HostConfig: { NetworkMode: 'none' },
|
|
NetworkSettings: { Networks: { none: {} } }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['nonet'];
|
|
expect(svc.network_mode).toBe('none');
|
|
expect(svc.networks).toBeUndefined();
|
|
});
|
|
|
|
test('network_mode: default produces no network_mode key', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/def',
|
|
Config: { Image: 'busybox:latest' },
|
|
HostConfig: { NetworkMode: 'default' },
|
|
NetworkSettings: { Networks: { bridge: {} } }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['def'];
|
|
expect(svc.network_mode).toBeUndefined();
|
|
expect(svc.networks).toBeUndefined();
|
|
});
|
|
|
|
test('serviceName option overrides the container name', () => {
|
|
const { name } = inspectToComposeService({ Name: '/original', Config: { Image: 'x' } }, { serviceName: 'renamed' });
|
|
expect(name).toBe('renamed');
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - env value $ escaping (#1507)', () => {
|
|
// docker compose interpolates $ in env values; an inspect value is already a resolved
|
|
// literal, so every $ must be escaped to $$ or compose eats it (a bcrypt hash
|
|
// $2a$12$... becomes $2a$12.<tail>, silently corrupting the password).
|
|
test('a bcrypt hash keeps every $ as $$ so compose interpolation leaves it intact', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/wg-easy',
|
|
Config: {
|
|
Image: 'ghcr.io/wg-easy/wg-easy:latest',
|
|
Env: ['PASSWORD_HASH=$2a$12$r68XHj7T7304UoWeYJc1BOSwjmyeBj.gzk7PnLOKv5AED8C5NNj5C']
|
|
},
|
|
HostConfig: {},
|
|
NetworkSettings: { Networks: {} }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['wg-easy'];
|
|
const entry = svc.environment.find((e: string) => e.startsWith('PASSWORD_HASH='));
|
|
expect(entry).toBe('PASSWORD_HASH=$$2a$$12$$r68XHj7T7304UoWeYJc1BOSwjmyeBj.gzk7PnLOKv5AED8C5NNj5C');
|
|
// After compose collapses $$ -> $, the original hash is recovered verbatim.
|
|
const value = entry.slice('PASSWORD_HASH='.length).replaceAll('$$', '$');
|
|
expect(value).toBe('$2a$12$r68XHj7T7304UoWeYJc1BOSwjmyeBj.gzk7PnLOKv5AED8C5NNj5C');
|
|
});
|
|
|
|
test('a value with no $ is left untouched', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'x', Env: ['WG_HOST=example.me', 'PORT=51820'] },
|
|
HostConfig: {},
|
|
NetworkSettings: { Networks: {} }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.environment).toContain('WG_HOST=example.me');
|
|
expect(svc.environment).toContain('PORT=51820');
|
|
});
|
|
|
|
test('a literal ${VAR}-looking value is escaped so compose does not expand it', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'x', Env: ['TEMPLATE=${NOT_A_VAR}-literal'] },
|
|
HostConfig: {},
|
|
NetworkSettings: { Networks: {} }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
const entry = svc.environment.find((e: string) => e.startsWith('TEMPLATE='));
|
|
expect(entry).toBe('TEMPLATE=$${NOT_A_VAR}-literal');
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - full network model (static IP, aliases, MAC)', () => {
|
|
test('static IPv4/IPv6, user aliases and MAC render as a per-network map', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx:1.27' },
|
|
HostConfig: {},
|
|
NetworkSettings: {
|
|
Networks: {
|
|
backend: {
|
|
Aliases: ['app', 'db.internal', 'a1b2c3d4e5f6'],
|
|
MacAddress: '02:42:ac:11:00:05',
|
|
IPAMConfig: { IPv4Address: '10.5.0.9', IPv6Address: 'fd00::9' }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
// 'app' (service key) and the 12-hex short id are stripped; 'db.internal' survives.
|
|
expect(svc.networks).toEqual({
|
|
backend: {
|
|
ipv4_address: '10.5.0.9',
|
|
ipv6_address: 'fd00::9',
|
|
aliases: ['db.internal'],
|
|
mac_address: '02:42:ac:11:00:05'
|
|
}
|
|
});
|
|
expect(svc.network_mode).toBeUndefined();
|
|
});
|
|
|
|
test('a compose service alias (com.docker.compose.service) is stripped as auto', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/proj-web-1',
|
|
Config: { Image: 'nginx', Labels: { 'com.docker.compose.service': 'web' } },
|
|
HostConfig: {},
|
|
NetworkSettings: {
|
|
Networks: { proj_default: { Aliases: ['proj-web-1', 'web'], IPAMConfig: { IPv4Address: '10.1.0.2' } } }
|
|
}
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['proj-web-1'];
|
|
// both 'proj-web-1' (name) and 'web' (compose service) are auto; only the static IP is left.
|
|
expect(svc.networks).toEqual({ proj_default: { ipv4_address: '10.1.0.2' } });
|
|
});
|
|
|
|
test('bare networks with no detail stay in the short list form (zero noise)', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: {},
|
|
NetworkSettings: { Networks: { frontend: { Aliases: ['app'] }, backend: {} } }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.networks).toEqual(['frontend', 'backend']);
|
|
});
|
|
|
|
test('dns_search and dns_opt are emitted alongside dns', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: { Dns: ['1.1.1.1'], DnsSearch: ['corp.local'], DnsOptions: ['ndots:2'] }
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.dns).toEqual(['1.1.1.1']);
|
|
expect(svc.dns_search).toEqual(['corp.local']);
|
|
expect(svc.dns_opt).toEqual(['ndots:2']);
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - resource limits', () => {
|
|
test('memory + cpu + pids limits map to the standalone top-level keys', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: {
|
|
Memory: 536870912,
|
|
MemoryReservation: 268435456,
|
|
MemorySwap: 1073741824,
|
|
NanoCpus: 1500000000,
|
|
CpusetCpus: '0-1',
|
|
PidsLimit: 200
|
|
}
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.mem_limit).toBe(536870912);
|
|
expect(svc.mem_reservation).toBe(268435456);
|
|
expect(svc.memswap_limit).toBe(1073741824);
|
|
expect(svc.cpus).toBe(1.5); // 1.5e9 nanocpus -> 1.5 cores
|
|
expect(svc.cpuset).toBe('0-1');
|
|
expect(svc.pids_limit).toBe(200);
|
|
});
|
|
|
|
test('daemon-default resource values are skipped', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: {
|
|
Memory: 0,
|
|
MemorySwap: 0,
|
|
NanoCpus: 0,
|
|
CpuShares: 1024, // neutral weight - not user intent
|
|
CpusetCpus: '',
|
|
PidsLimit: -1 // unlimited (the default)
|
|
}
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.mem_limit).toBeUndefined();
|
|
expect(svc.memswap_limit).toBeUndefined();
|
|
expect(svc.cpus).toBeUndefined();
|
|
expect(svc.cpu_shares).toBeUndefined();
|
|
expect(svc.cpuset).toBeUndefined();
|
|
expect(svc.pids_limit).toBeUndefined();
|
|
});
|
|
|
|
test('cpu_shares is emitted for a non-neutral weight', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: { CpuShares: 512 }
|
|
}).doc.services['app'];
|
|
expect(svc.cpu_shares).toBe(512);
|
|
});
|
|
|
|
test('memswap_limit -1 (unlimited swap) is a real user value and is emitted', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: { Memory: 536870912, MemorySwap: -1 }
|
|
}).doc.services['app'];
|
|
expect(svc.memswap_limit).toBe(-1);
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - lifecycle (stop/expose/group_add/init)', () => {
|
|
test('stop_signal + stop_grace_period from a non-default StopSignal/StopTimeout', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx', StopSignal: 'SIGINT', StopTimeout: 30 },
|
|
HostConfig: {}
|
|
}).doc.services['app'];
|
|
expect(svc.stop_signal).toBe('SIGINT');
|
|
expect(svc.stop_grace_period).toBe('30s');
|
|
});
|
|
|
|
test('default SIGTERM and null StopTimeout are skipped', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx', StopSignal: 'SIGTERM', StopTimeout: null },
|
|
HostConfig: {}
|
|
}).doc.services['app'];
|
|
expect(svc.stop_signal).toBeUndefined();
|
|
expect(svc.stop_grace_period).toBeUndefined();
|
|
});
|
|
|
|
test('expose lists ports that are exposed but neither published nor image-baked', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx', ExposedPorts: { '80/tcp': {}, '9000/tcp': {}, '53/udp': {} } },
|
|
HostConfig: { PortBindings: { '80/tcp': [{ HostPort: '8080' }] } }
|
|
};
|
|
// image itself EXPOSEs 9000; that must not leak into expose.
|
|
const svc = parseCompose(inspect, { imageExposedPorts: { '9000/tcp': {} } }).doc.services['app'];
|
|
// 80 is published (in ports), 9000 is image-baked -> only 53/udp remains.
|
|
expect(svc.expose).toEqual(['53/udp']);
|
|
});
|
|
|
|
test('group_add and init: true are emitted', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: { GroupAdd: ['docker'], Init: true }
|
|
}).doc.services['app'];
|
|
expect(svc.group_add).toEqual(['docker']);
|
|
expect(svc.init).toBe(true);
|
|
});
|
|
|
|
test('init: null / false is not emitted', () => {
|
|
const svcNull = parseCompose({ Name: '/a', Config: { Image: 'x' }, HostConfig: { Init: null } }).doc.services['a'];
|
|
const svcFalse = parseCompose({ Name: '/b', Config: { Image: 'x' }, HostConfig: { Init: false } }).doc.services['b'];
|
|
expect(svcNull.init).toBeUndefined();
|
|
expect(svcFalse.init).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('inspectToCompose - runtime/security options', () => {
|
|
test('security_opt, pid, userns, cgroup_parent, runtime, shm_size, volumes_from, oom, domainname', () => {
|
|
const inspect: DockerInspect = {
|
|
Name: '/app',
|
|
Config: { Image: 'nginx', Domainname: 'corp.local' },
|
|
HostConfig: {
|
|
SecurityOpt: ['no-new-privileges'],
|
|
PidMode: 'host',
|
|
UsernsMode: 'host',
|
|
CgroupParent: '/custom.slice',
|
|
Runtime: 'nvidia',
|
|
ShmSize: 134217728, // 128MiB, differs from the 64MiB default
|
|
VolumesFrom: ['data-container'],
|
|
OomKillDisable: true,
|
|
OomScoreAdj: -500
|
|
}
|
|
};
|
|
const svc = parseCompose(inspect).doc.services['app'];
|
|
expect(svc.security_opt).toEqual(['no-new-privileges']);
|
|
expect(svc.pid).toBe('host');
|
|
expect(svc.userns_mode).toBe('host');
|
|
expect(svc.cgroup_parent).toBe('/custom.slice');
|
|
expect(svc.runtime).toBe('nvidia');
|
|
expect(svc.shm_size).toBe(134217728);
|
|
expect(svc.volumes_from).toEqual(['data-container']);
|
|
expect(svc.oom_kill_disable).toBe(true);
|
|
expect(svc.oom_score_adj).toBe(-500);
|
|
expect(svc.domainname).toBe('corp.local');
|
|
});
|
|
|
|
test('runc runtime, 64MiB shm_size, and oom defaults are skipped', () => {
|
|
const svc = parseCompose({
|
|
Name: '/app',
|
|
Config: { Image: 'nginx' },
|
|
HostConfig: {
|
|
Runtime: 'runc',
|
|
ShmSize: 67108864, // 64MiB default
|
|
OomKillDisable: false,
|
|
OomScoreAdj: 0
|
|
}
|
|
}).doc.services['app'];
|
|
expect(svc.runtime).toBeUndefined();
|
|
expect(svc.shm_size).toBeUndefined();
|
|
expect(svc.oom_kill_disable).toBeUndefined();
|
|
expect(svc.oom_score_adj).toBeUndefined();
|
|
});
|
|
});
|