Skip to content

Commit 943b4a2

Browse files
committed
Added getDockerContainers
1 parent 3993d59 commit 943b4a2

File tree

3 files changed

+443
-1
lines changed

3 files changed

+443
-1
lines changed

src/__tests__/Docker.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { getDockerContainers } from '../docker';
2+
3+
test('getDockerContainers', async () => {
4+
const containers = await getDockerContainers(true);
5+
console.log(JSON.stringify(containers, null, 2)); // TODO REMOVE
6+
});

src/docker.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,192 @@
1+
import { execCommand, parseByteValue, parseDate, processKeyValueString } from "./utils";
2+
3+
export type PortMapping = {
4+
/** If listening on the host is bound to a particular hostname or ip. If not present all interfaces are accepted. */
5+
hostAddress?: string;
6+
7+
/** Source port on the host to listen at. */
8+
hostPort: number;
9+
10+
/** Port inside the container to forward to. */
11+
containerPort: number;
12+
13+
/** Protocol that is mapped. */
14+
protocol: 'tcp' | 'udp';
15+
};
16+
117
export type DockerContainer = {
218

3-
};
19+
/** Command running in the container. */
20+
cmd: string;
21+
22+
/** Date when the container was created. */
23+
createdAt: Date;
24+
25+
/** ID of the container. */
26+
containerId: string;
27+
28+
/** Name or ID of the image used by the container. */
29+
image: string;
30+
31+
/** Labels assigned to the container. */
32+
labels: {[key: string]: any};
33+
34+
/** Number of local volumes attached to the container. */
35+
localVolumeCount: number;
36+
37+
/** Local directories or volume names mounted into the container. */
38+
mounts: string[];
39+
40+
/** Name of the container. */
41+
name: string;
42+
43+
/** Names of the networks the container is attached to. */
44+
networks: string[];
45+
46+
/** Exposed ports for network traffic. */
47+
ports: PortMapping[];
48+
49+
/** Humand-readable string when the container has been cretated. */
50+
runningForMessage: string;
51+
52+
/** Disk size the container utilized in bytes. */
53+
size: number;
54+
55+
/** Virtual size of the container, if available. */
56+
virtualSize?: number;
57+
58+
/**
59+
* State of the container. Possible values are:
60+
* - 'created': The container has been created but not started.
61+
* - 'restarting': The container is restarting.
62+
* - 'running': The container is currently running.
63+
* - 'removing': The container is being removed.
64+
* - 'paused': The container is paused.
65+
* - 'exited': The container has exited.
66+
* - 'dead': The container is dead and cannot be restarted.
67+
*/
68+
state: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead';
69+
70+
/** Human-readable string describing the current status of the container. */
71+
statusMessage: string;
72+
73+
/** Health state of the container if provided. */
74+
healthState?: 'starting' | 'healthy' | 'unhealthy';
75+
76+
/** Whether the container is currently running. */
77+
isRunning: boolean;
78+
79+
/** Whether the container is healthy. Only false if not running or unhealthy. */
80+
isHealthy: boolean;
81+
};
82+
83+
84+
export async function getDockerContainers(includeStopped: boolean = false): Promise<DockerContainer[]> {
85+
return await execCommand(
86+
'docker ps --format json --no-trunc' + (includeStopped ? ' -a' : ''),
87+
).then(output => {
88+
// Docker has a custom format with one JSON object per line
89+
return output.split('\n').filter(line => line.trim() !== '').map<DockerContainer>(line => {
90+
const j = JSON.parse(line);
91+
92+
// post-process dates
93+
if(j.CreatedAt && typeof j.CreatedAt === 'string'){
94+
j.CreatedAt = parseDate(j.CreatedAt) || j.CreatedAt;
95+
}
96+
97+
// post-process labels
98+
if(j.Labels && typeof j.Labels === 'string') {
99+
j.Labels = processKeyValueString(j.Labels, ',', '=');
100+
}
101+
102+
// post-process local volumes
103+
if(j.LocalVolumes && typeof j.LocalVolumes === 'string') {
104+
j.LocalVolumes = parseInt(j.LocalVolumes, 10);
105+
j.LocalVolumes = Number.isNaN(j.LocalVolumes) ? j.LocalVolumes : j.LocalVolumes;
106+
}
107+
108+
// post-process mounts
109+
if(j.Mounts && typeof j.Mounts === 'string') {
110+
j.Mounts = (j.Mounts as string).split(',').map(mount => mount.trim());
111+
}
112+
113+
// post-process networks
114+
if(j.Networks && typeof j.Networks === 'string') {
115+
j.Networks = (j.Networks as string).split(',').map(network => network.trim());
116+
}
117+
118+
// post-process ports
119+
if(j.Ports && typeof j.Ports === 'string') {
120+
const portMappings: PortMapping[] = [];
121+
(j.Ports as string).split(',').forEach(str => {
122+
const [mapping, protocol = 'tcp'] = str.split('/');
123+
const [host, containerPortStr] = mapping.split('->');
124+
const containerPort: number = parseInt(containerPortStr, 10);
125+
const hostIdx = host.lastIndexOf(':');
126+
let hostPort: number = containerPort;
127+
let hostAddress: string | undefined = undefined;
128+
if(hostIdx > 0){
129+
hostPort = parseInt(host.substring(hostIdx + 1), 10);
130+
hostAddress = host.substring(0, hostIdx);
131+
hostAddress = (hostAddress && hostAddress !== '0.0.0.0' && hostAddress !== '::') ? hostAddress : undefined;
132+
} else {
133+
hostPort = parseInt(host, 10);
134+
}
135+
const pm: PortMapping = { hostAddress, hostPort, containerPort, protocol: protocol as 'tcp' | 'udp' };
136+
137+
// check if port mapping already exists
138+
const alreadyExists = portMappings.some(o =>
139+
o.hostAddress === pm.hostAddress &&
140+
o.hostPort === pm.hostPort &&
141+
o.containerPort === pm.containerPort &&
142+
o.protocol === pm.protocol
143+
);
144+
if(!alreadyExists) portMappings.push(pm);
145+
});
146+
j.Ports = portMappings;
147+
}
148+
149+
// post-process size
150+
if(j.Size && typeof j.Size === 'string') {
151+
const idx = (j.Size as string).indexOf('irtual');
152+
if(idx > 0) {
153+
j.VirtualSize = parseByteValue((j.Size as string).substring(idx + 6));
154+
}
155+
j.Size = parseByteValue((j.Size as string));
156+
}
157+
158+
// post-process status
159+
if(j.Status && typeof j.Status === 'string'){
160+
if((j.Status as string).includes('unhealthy')){
161+
j.HealthState = 'unhealthy';
162+
} else if((j.Status as string).includes('healthy')){
163+
j.HealthState = 'healthy';
164+
} else if((j.Status as string).includes('starting')){
165+
j.HealthState = 'starting';
166+
}
167+
}
168+
169+
const containerInfo: DockerContainer = {
170+
cmd: j.Command,
171+
createdAt: j.CreatedAt,
172+
containerId: j.ID,
173+
image: j.Image,
174+
labels: j.Labels || {},
175+
localVolumeCount: j.LocalVolumes || 0,
176+
mounts: j.Mounts || [],
177+
name: j.Names,
178+
networks: j.Networks || [],
179+
ports: j.Ports || [],
180+
runningForMessage: j.RunningFor || '',
181+
size: j.Size || 0,
182+
state: j.State,
183+
statusMessage: j.Status,
184+
isRunning: (j.State === 'running'),
185+
isHealthy: (j.HealthState === 'healthy' || (j.State === 'running' && !j.HealthState)),
186+
};
187+
if(j.HealthState) containerInfo.healthState = j.HealthState;
188+
if(j.VirtualSize) containerInfo.virtualSize = j.VirtualSize;
189+
return containerInfo;
190+
});
191+
}).catch(() => []);
192+
}

0 commit comments

Comments
 (0)