Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ps supports json format #256

Merged
merged 3 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,67 @@ compose.exec('node', 'npm install', { cwd: path.join(__dirname) })
* `[composeOptions] string[]|Array<string|string[]`: pass optional compose options like `"--verbose"` or `[["--verbose"], ["--log-level", "DEBUG"]]` or `["--verbose", ["--loglevel", "DEBUG"]]` for *all* commands.
* `[callback] (chunk: Buffer, sourceStream?: 'stdout' | 'stderr') => void`: optional callback function, that provides infromation about the process while it is still runing.
* `[commandOptions] string[]|Array<string|string[]`: pass optional command options like `"--build"` or `[["--build"], ["--timeout", "5"]]` or `["--build", ["--timeout", "5"]]` for the `up` command. Viable `commandOptions` depend on the command (`up`, `down` etc.) itself

### `ps`

`ps(options)` - Lists containers for a Compose project, with current status and exposed ports.

`ps` returns a `Promise` of `TypedDockerComposeResult<DockerComposePsResult>`.

A basic example looks like this:

```javascript
const result = await compose.ps({ cwd: path.join(__dirname) })
result.data.services.forEach((service) => {
console.log(service.name, service.command, service.state, service.ports)
// state is e.g. 'Up 2 hours'
})
```

The resolved `result` might look like this (for v2):

```javascript
{
exitCode: 0,
err: '',
out: 'NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS\n' +
`compose_test_proxy nginx:1.19.9-alpine "/docker-entrypoint.sh nginx -g 'daemon off;'" proxy 1 second ago Up Less than a second 80/tcp\n` +
`compose_test_web nginx:1.16.0 "nginx -g 'daemon off;'" web 1 second ago Up Less than a second 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\n`,
data: {
services: [
{
name: 'compose_test_proxy',
command: `"/docker-entrypoint.sh nginx -g 'daemon off;'"`,
state: 'Up Less than a second',
ports: [ { exposed: { port: 80, protocol: 'tcp' } } ]
},
{
name: 'compose_test_web',
command: `"nginx -g 'daemon off;'"`,
state: 'Up Less than a second',
ports: [
{
exposed: { port: 80, protocol: 'tcp' },
mapped: { port: 80, address: '0.0.0.0' }
},
{
exposed: { port: 443, protocol: 'tcp' },
mapped: { port: 443, address: '0.0.0.0' }
}
]
}
]
}
}
```

**Only v2**: If you need a defined state, you can use the `--format json` command option.
This will return one of the defined states `paused | restarting | removing | running | dead | created | exited` as the state of a service.

```javascript
const result = await compose.ps({ cwd: path.join(__dirname), commandOptions: [["--format", "json"]] })
result.data.services.forEach((service) => {
console.log(service.name, service.command, service.state, service.ports)
// state is one of the defined states: paused | restarting | removing | running | dead | created | exited
})
```
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ To execute command inside a running container
compose.exec('node', 'npm install', { cwd: path.join(__dirname) })
```

To list the containers for a compose project

```javascript
const result = await compose.ps({ cwd: path.join(__dirname) })
result.data.services.forEach((service) => {
console.log(service.name, service.command, service.state, service.ports)
// state is e.g. 'Up 2 hours'
})
```

The docker-compose v2 version also support the `--format json` command option to get a better state support:

```javascript
const result = await compose.ps({ cwd: path.join(__dirname), commandOptions: [["--format", "json"]] })
result.data.services.forEach((service) => {
console.log(service.name, service.command, service.state, service.ports)
// state is one of the defined states: paused | restarting | removing | running | dead | created | exited
})
```

## Known issues with v2 support

* During testing we noticed that `docker compose` seems to send it's exit code also commands don't seem to have finished. This doesn't occur for all commands, but for example with `stop` or `down`. We had the option to wait for stopped / removed containers using third party libraries but this would make bootstrapping `docker-compose` much more complicated for the users. So we decided to use a `setTimeout(500)` workaround. We're aware this is not perfect but it seems to be the most appropriate solution for now. Details can be found in the [v2 PR discussion](https://github.com/PDMLab/docker-compose/pull/228#issuecomment-1422895821) (we're happy to get help here).
Expand Down
65 changes: 43 additions & 22 deletions src/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,51 +66,72 @@ export type TypedDockerComposeResult<T> = {

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 type DockerComposePsResultService = {
name: string
command: string
state: string
ports: Array<{
mapped?: { address: string; port: number }
exposed: { port: number; protocol: string }
}>
}

export type DockerComposePsResult = {
services: Array<DockerComposePsResultService>
}

const arrayIncludesTuple = (
arr: string[] | (string | string[])[],
tuple: string[]
) => {
return arr.some(
(subArray) =>
Array.isArray(subArray) &&
subArray.length === tuple.length &&
subArray.every((value, index) => value === tuple[index])
)
}

export const mapPsOutput = (
output: string,
options?: IDockerComposeOptions
): DockerComposePsResult => {
let isQuiet = false
let isJson = false
if (options?.commandOptions) {
isQuiet =
options.commandOptions.includes('-q') ||
options.commandOptions.includes('--quiet') ||
options.commandOptions.includes('--services')

isJson = arrayIncludesTuple(options.commandOptions, ['--format', 'json'])
}
const services = output
.split(`\n`)
.filter(nonEmptyString)
.filter((_, index) => isQuiet || index >= 1)
.filter((_, index) => isQuiet || isJson || index >= 1)
.map((line) => {
let nameFragment = line
let commandFragment = ''
let imageFragment = ''
let serviceFragment = ''
let createdFragment = ''
let stateFragment = ''
let untypedPortsFragment = ''
if (!isQuiet) {
;[
nameFragment,
imageFragment,
commandFragment,
serviceFragment,
createdFragment,
stateFragment,
untypedPortsFragment
] = line.split(/\s{3,}/)
if (isJson) {
const serviceLine = JSON.parse(line)
nameFragment = serviceLine.Name
commandFragment = serviceLine.Command
stateFragment = serviceLine.State
untypedPortsFragment = serviceLine.Ports
} else {
const lineColumns = line.split(/\s{3,}/)
// the line has the columns in the following order:
// NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
// @see https://docs.docker.com/engine/reference/commandline/compose_ps/#description
nameFragment = lineColumns[0]
commandFragment = lineColumns[2]
stateFragment = lineColumns[5]
untypedPortsFragment = lineColumns[6]
}
}
return {
name: nameFragment.trim(),
Expand Down
28 changes: 28 additions & 0 deletions test/v2/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,35 @@ describe('when calling ps command', (): void => {
const web = std.data.services.find(
(service) => service.name === 'compose_test_web'
)
expect(web?.command).toContain('nginx') // Note: actually it contains "nginx -g 'daemon off;'"
expect(web?.state).toContain('Up')
expect(web?.ports.length).toBe(2)
expect(web?.ports[1].exposed.port).toBe(443)
expect(web?.ports[1].exposed.protocol).toBe('tcp')
expect(web?.ports[1].mapped?.port).toBe(443)
expect(web?.ports[1].mapped?.address).toBe('0.0.0.0')
await compose.down({ cwd: path.join(__dirname), log: logOutput })
})

it('ps shows status data for started containers using json format', async (): Promise<void> => {
await compose.upAll({ cwd: path.join(__dirname), log: logOutput })
// await new Promise((resolve) => setTimeout(resolve, 2000))

const std = await compose.ps({
cwd: path.join(__dirname),
log: logOutput,
commandOptions: [['--format', 'json']]
})

const running = await getRunningContainers()

expect(std.err).toBeFalsy()
expect(std.data.services.length).toBe(2)
const web = std.data.services.find(
(service) => service.name === 'compose_test_web'
)
expect(web?.command).toContain('nginx') // Note: actually it contains "nginx -g 'daemon off;'"
expect(web?.state).toBe('running')
expect(web?.ports.length).toBe(2)
expect(web?.ports[1].exposed.port).toBe(443)
expect(web?.ports[1].exposed.protocol).toBe('tcp')
Expand Down