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+
117export 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