compose generated from a container now captures network config and resource limits
This commit is contained in:
11 files changed
+831
-39
No files matched your search
@@ -55,7 +55,12 @@ describe('inspectToCompose - real gitea inspect (compose-managed, named-vol, cus
|
||||
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'];
|
||||
expect(svc.networks).toEqual(['semver-demo-stack_default']);
|
||||
// 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();
|
||||
@@ -128,14 +133,17 @@ describe('inspectToCompose - real nginx inspect (published ports, bind :ro, name
|
||||
expect(labels['my.custom.label']).toBe('hello');
|
||||
});
|
||||
|
||||
test('auto-generated noise (hostname, working_dir /, shm_size) is not emitted', () => {
|
||||
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;
|
||||
// shm_size is host-derived -> never emitted.
|
||||
// Hostname is the short container id (12-hex) -> dropped; working_dir is "/" -> dropped.
|
||||
expect(svc.hostname).toBeUndefined();
|
||||
expect(svc.working_dir).toBeUndefined();
|
||||
expect(svc.shm_size).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', () => {
|
||||
@@ -196,8 +204,277 @@ describe('inspectToCompose - default-noise filtering', () => {
|
||||
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 - 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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user