mirror of
https://github.com/danbulant/docker-compose
synced 2026-05-24 12:35:32 +00:00
feat: make result for ps command type safe
This commit is contained in:
parent
ed91eba770
commit
880d2522b5
3 changed files with 153 additions and 8 deletions
70
src/index.ts
70
src/index.ts
|
|
@ -62,6 +62,61 @@ export type TypedDockerComposeResult<T> = {
|
||||||
|
|
||||||
const nonEmptyString = (v: string) => v !== ''
|
const nonEmptyString = (v: string) => v !== ''
|
||||||
|
|
||||||
|
export type DockerComposePsResult = {
|
||||||
|
services: Array<{
|
||||||
|
name: string
|
||||||
|
command: string
|
||||||
|
state: string
|
||||||
|
ports: Array<{
|
||||||
|
mapped?: { address: string; port: number }
|
||||||
|
exposed: { port: number; protocol: string }
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapPorts = (
|
||||||
|
ports: string
|
||||||
|
): Array<{
|
||||||
|
mapped?: { address: string; port: number }
|
||||||
|
exposed: { port: number; protocol: string }
|
||||||
|
}> => {
|
||||||
|
if (!ports) return []
|
||||||
|
const allPorts = ports.split(',')
|
||||||
|
return allPorts.map((portString) => {
|
||||||
|
const exposedFragments = portString.trim().split('->')
|
||||||
|
const [port, protocol] =
|
||||||
|
exposedFragments.length === 1
|
||||||
|
? exposedFragments[0].split('/')
|
||||||
|
: exposedFragments[1].split('/')
|
||||||
|
const [address, mappedPort] =
|
||||||
|
exposedFragments.length === 2 ? exposedFragments[0].split(':') : []
|
||||||
|
const result = {
|
||||||
|
exposed: { port: Number(port), protocol },
|
||||||
|
...(address &&
|
||||||
|
mappedPort && { mapped: { port: Number(mappedPort), address } })
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapPsOutput = (output: string): DockerComposePsResult => {
|
||||||
|
const lines = output.split(`\n`).filter(nonEmptyString)
|
||||||
|
const lines2 = lines.filter((line, index) => index > 1)
|
||||||
|
const services = lines2.map((line) => {
|
||||||
|
const [nameString, commandString, stateString, allPortsString] = line.split(
|
||||||
|
/\s{3,}/
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: nameString.trim(),
|
||||||
|
command: commandString.trim(),
|
||||||
|
state: stateString.trim(),
|
||||||
|
ports: mapPorts(allPortsString.trim())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { services }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts supplied yml files to cli arguments
|
* Converts supplied yml files to cli arguments
|
||||||
* https://docs.docker.com/compose/reference/overview/#use--f-to-specify-name-and-path-of-one-or-more-compose-files
|
* https://docs.docker.com/compose/reference/overview/#use--f-to-specify-name-and-path-of-one-or-more-compose-files
|
||||||
|
|
@ -356,10 +411,19 @@ export const configVolumes = async function (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ps = function (
|
export const ps = async function (
|
||||||
options?: IDockerComposeOptions
|
options?: IDockerComposeOptions
|
||||||
): Promise<IDockerComposeResult> {
|
): Promise<TypedDockerComposeResult<DockerComposePsResult>> {
|
||||||
return execCompose('ps', [], options)
|
try {
|
||||||
|
const result = await execCompose('ps', [], options)
|
||||||
|
const data = mapPsOutput(result.out)
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const push = function (
|
export const push = function (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ services:
|
||||||
image: nginx:1.16.0
|
image: nginx:1.16.0
|
||||||
container_name: compose_test_web
|
container_name: compose_test_web
|
||||||
command: 'nginx -g "daemon off;"'
|
command: 'nginx -g "daemon off;"'
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
proxy:
|
proxy:
|
||||||
image: nginx:1.19.9-alpine
|
image: nginx:1.19.9-alpine
|
||||||
container_name: compose_test_proxy
|
container_name: compose_test_proxy
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import Docker from 'dockerode'
|
||||||
import * as compose from '../src/index'
|
import * as compose from '../src/index'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { readFile } from 'fs'
|
import { readFile } from 'fs'
|
||||||
|
import { mapPorts, mapPsOutput } from '../src/index'
|
||||||
const docker = new Docker()
|
const docker = new Docker()
|
||||||
|
|
||||||
// Docker commands, especially builds, can take some time. This makes sure that they can take the time they need.
|
// Docker commands, especially builds, can take some time. This makes sure that they can take the time they need.
|
||||||
|
|
@ -551,8 +552,16 @@ test('ps shows status data for started containers', async (): Promise<void> => {
|
||||||
const std = await compose.ps({ cwd: path.join(__dirname), log: logOutput })
|
const std = await compose.ps({ cwd: path.join(__dirname), log: logOutput })
|
||||||
|
|
||||||
expect(std.err).toBeFalsy()
|
expect(std.err).toBeFalsy()
|
||||||
expect(std.out.includes('compose_test_web')).toBeTruthy()
|
expect(std.data.services.length).toBe(3)
|
||||||
expect(std.out.includes('compose_test_proxy')).toBeTruthy()
|
const web = std.data.services.find(
|
||||||
|
(service) => service.name === 'compose_test_web'
|
||||||
|
)
|
||||||
|
expect(std.data.services.length).toBe(3)
|
||||||
|
expect(web?.ports.length).toBe(2)
|
||||||
|
expect(web?.ports[0].exposed.port).toBe(443)
|
||||||
|
expect(web?.ports[0].exposed.protocol).toBe('tcp')
|
||||||
|
expect(web?.ports[0].mapped?.port).toBe(443)
|
||||||
|
expect(web?.ports[0].mapped?.address).toBe('0.0.0.0')
|
||||||
await compose.down({ cwd: path.join(__dirname), log: logOutput })
|
await compose.down({ cwd: path.join(__dirname), log: logOutput })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -563,8 +572,14 @@ test('ps does not show status data for stopped containers', async (): Promise<vo
|
||||||
const std = await compose.ps({ cwd: path.join(__dirname), log: logOutput })
|
const std = await compose.ps({ cwd: path.join(__dirname), log: logOutput })
|
||||||
|
|
||||||
expect(std.err).toBeFalsy()
|
expect(std.err).toBeFalsy()
|
||||||
expect(std.out.includes('compose_test_web')).toBeTruthy()
|
const web = std.data.services.find(
|
||||||
expect(std.out.includes('compose_test_proxy')).toBeFalsy()
|
(service) => service.name === 'compose_test_web'
|
||||||
|
)
|
||||||
|
const proxy = std.data.services.find(
|
||||||
|
(service) => service.name === 'compose_test_proxy'
|
||||||
|
)
|
||||||
|
expect(web?.name).toBe('compose_test_web')
|
||||||
|
expect(proxy).toBeFalsy()
|
||||||
await compose.down({ cwd: path.join(__dirname), log: logOutput })
|
await compose.down({ cwd: path.join(__dirname), log: logOutput })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -649,3 +664,66 @@ test('returns version information', async (): Promise<void> => {
|
||||||
|
|
||||||
expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)$/)
|
expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)$/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parse ps output', () => {
|
||||||
|
const output = ` Name Command State Ports \n-------------------------------------------------------------------------------------------------------\ncompose_test_hello /hello Exit 0 \ncompose_test_proxy /docker-entrypoint.sh ngin ... Up 80/tcp \ncompose_test_web nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp\n`
|
||||||
|
|
||||||
|
const psOut = mapPsOutput(output)
|
||||||
|
expect(psOut.services[0]).toEqual({
|
||||||
|
command: '/hello',
|
||||||
|
name: 'compose_test_hello',
|
||||||
|
state: 'Exit 0',
|
||||||
|
ports: []
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(psOut.services[1]).toEqual({
|
||||||
|
command: '/docker-entrypoint.sh ngin ...',
|
||||||
|
name: 'compose_test_proxy',
|
||||||
|
state: 'Up',
|
||||||
|
ports: [{ exposed: { port: 80, protocol: 'tcp' } }]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(psOut.services[2]).toEqual({
|
||||||
|
command: 'nginx -g daemon off;',
|
||||||
|
name: 'compose_test_web',
|
||||||
|
state: 'Up',
|
||||||
|
ports: [
|
||||||
|
{
|
||||||
|
exposed: { port: 443, protocol: 'tcp' },
|
||||||
|
mapped: { port: 443, address: '0.0.0.0' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exposed: { port: 80, protocol: 'tcp' },
|
||||||
|
mapped: { port: 80, address: '0.0.0.0' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parse ports', () => {
|
||||||
|
const noPort = ''
|
||||||
|
const exposedTcp = '80/tcp'
|
||||||
|
const mappedExposedTcp = '0.0.0.0:443->443/tcp'
|
||||||
|
const multipleExposedMappedTcp = '0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp'
|
||||||
|
|
||||||
|
expect(mapPorts(noPort)).toEqual([])
|
||||||
|
expect(mapPorts(exposedTcp)).toEqual([
|
||||||
|
{ exposed: { port: 80, protocol: 'tcp' } }
|
||||||
|
])
|
||||||
|
expect(mapPorts(mappedExposedTcp)).toEqual([
|
||||||
|
{
|
||||||
|
exposed: { port: 443, protocol: 'tcp' },
|
||||||
|
mapped: { address: '0.0.0.0', port: 443 }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
expect(mapPorts(multipleExposedMappedTcp)).toEqual([
|
||||||
|
{
|
||||||
|
exposed: { port: 443, protocol: 'tcp' },
|
||||||
|
mapped: { address: '0.0.0.0', port: 443 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exposed: { port: 80, protocol: 'tcp' },
|
||||||
|
mapped: { address: '0.0.0.0', port: 80 }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue