From 766a8f3bfd92333857ff97869637e84e61a80c64 Mon Sep 17 00:00:00 2001 From: Tim Kask Date: Mon, 30 Oct 2023 13:28:15 +0100 Subject: [PATCH 1/3] feat: ps supports json format --- src/v2.ts | 65 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/src/v2.ts b/src/v2.ts index 48fd6700..b65fe14e 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -66,51 +66,72 @@ export type TypedDockerComposeResult = { 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 +} + +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(), From 8498f40196e795c77aa5ec4234e1f6c1856dcba6 Mon Sep 17 00:00:00 2001 From: Tim Kask Date: Mon, 30 Oct 2023 14:32:28 +0100 Subject: [PATCH 2/3] test: adding test for ps with format json --- test/v2/compose.test.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/v2/compose.test.ts b/test/v2/compose.test.ts index 4da36702..09c58d55 100644 --- a/test/v2/compose.test.ts +++ b/test/v2/compose.test.ts @@ -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 => { + 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') From bc248ceed6af8dabe0c724c3d1297e04625078a4 Mon Sep 17 00:00:00 2001 From: Tim Kask Date: Mon, 30 Oct 2023 15:03:17 +0100 Subject: [PATCH 3/3] docs: adding documentation for ps with format json --- docs/api.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 20 +++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/docs/api.md b/docs/api.md index b220d2ee..2c70a6a3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -97,3 +97,67 @@ compose.exec('node', 'npm install', { cwd: path.join(__dirname) }) * `[composeOptions] string[]|Array void`: optional callback function, that provides infromation about the process while it is still runing. * `[commandOptions] string[]|Array`. + +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 +}) +``` diff --git a/readme.md b/readme.md index 9d2c6ca1..1f461691 100644 --- a/readme.md +++ b/readme.md @@ -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).