From f1e9620c1642ffc6bb631ef83afc71a63bde2485 Mon Sep 17 00:00:00 2001 From: alex-Symbroson Date: Wed, 13 Mar 2024 14:17:52 +0100 Subject: [PATCH 1/5] support docker-desktop version in tests --- test/v1/index.test.ts | 2 +- test/v2/compose.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/v1/index.test.ts b/test/v1/index.test.ts index b23f21a8..e6ba5fb0 100644 --- a/test/v1/index.test.ts +++ b/test/v1/index.test.ts @@ -701,7 +701,7 @@ test('removes container', async (): Promise => { test('returns version information', async (): Promise => { const version = (await compose.version()).data.version - expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)$/) + expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)?(\+.*)*(-\w+(\.\d+))?$/) }) test('parse ps output', () => { diff --git a/test/v2/compose.test.ts b/test/v2/compose.test.ts index b602ac88..f0fc3d3f 100644 --- a/test/v2/compose.test.ts +++ b/test/v2/compose.test.ts @@ -866,7 +866,7 @@ describe('version command', (): void => { it('returns version information', async (): Promise => { const version = (await compose.version()).data.version - expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)?(\+.*)*$/) + expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)?(\+.*)*(-\w+(\.\d+))?$/) }) }) From ce2897baf714122b93eaff5dff6519a677d14a24 Mon Sep 17 00:00:00 2001 From: alex-Symbroson Date: Wed, 13 Mar 2024 14:21:54 +0100 Subject: [PATCH 2/5] add `compose images` support --- src/v2.ts | 85 +++++++++++++++++++++++++++ test/v2/compose.test.ts | 125 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/src/v2.ts b/src/v2.ts index 65ab9a6b..1570138f 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -81,10 +81,21 @@ export type DockerComposePsResultService = { }> } +export type DockerComposeImListResultService = { + container: string + repository: string + tag: string + id: string // 12 Byte id +} + export type DockerComposePsResult = { services: Array } +export type DockerComposeImListResult = { + services: Array +} + const arrayIncludesTuple = ( arr: string[] | (string | string[])[], tuple: string[] @@ -148,6 +159,63 @@ export const mapPsOutput = ( return { services } } +export const mapImListOutput = ( + output: string, + options?: IDockerComposeOptions +): DockerComposeImListResult => { + let isQuiet = false + let isJson = false + if (options?.commandOptions) { + isQuiet = + options.commandOptions.includes('-q') || + options.commandOptions.includes('--quiet') + + isJson = arrayIncludesTuple(options.commandOptions, ['--format', 'json']) + } + + if (isJson) { + const data = JSON.parse(output) + const services = data.map((serviceLine) => { + let idFragment = serviceLine.ID + // trim json 64B id format "type:id" to 12B id + const idTypeIndex = idFragment.indexOf(':') + if (idTypeIndex > 0) + idFragment = idFragment.slice(idTypeIndex + 1, idTypeIndex + 13) + + return { + container: serviceLine.ContainerName, + repository: serviceLine.Repository, + tag: serviceLine.Tag, + id: idFragment + } + }) + return { services } + } + + const services = output + .split(`\n`) + .filter(nonEmptyString) + .filter((_, index) => isQuiet || isJson || index >= 1) + .map((line) => { + // the line has the columns in the following order: + // CONTAINER REPOSITORY TAG IMAGE ID SIZE + const lineColumns = line.split(/\s{3,}/) + + let containerFragment = lineColumns[0] || line + let repositoryFragment = lineColumns[1] || '' + let tagFragment = lineColumns[2] || '' + let idFragment = lineColumns[3] || '' + + return { + container: containerFragment.trim(), + repository: repositoryFragment.trim(), + tag: tagFragment.trim(), + id: idFragment.trim() + } as DockerComposeImListResultService + }) + return { services } +} + /** * 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 @@ -508,6 +576,23 @@ export const ps = async function ( } } +export const image = { + list: async function ( + options?: IDockerComposeOptions + ): Promise> { + try { + const result = await execCompose('images', [], options) + const data = mapImListOutput(result.out, options) + return { + ...result, + data + } + } catch (error) { + return Promise.reject(error) + } + } +} + export const push = function ( options: IDockerComposePushOptions = {} ): Promise { diff --git a/test/v2/compose.test.ts b/test/v2/compose.test.ts index f0fc3d3f..b759829b 100644 --- a/test/v2/compose.test.ts +++ b/test/v2/compose.test.ts @@ -3,7 +3,7 @@ import Docker, { ContainerInfo } from 'dockerode' import * as compose from '../../src/v2' import * as path from 'path' import { readFile } from 'fs' -import { mapPsOutput } from '../../src/v2' +import { mapPsOutput, mapImListOutput } from '../../src/v2' const docker = new Docker() const isContainerRunning = async (name: string): Promise => @@ -774,6 +774,66 @@ describe('when calling ps command', (): void => { }, 15000) }) +describe('when calling image list command', (): void => { + it('image list shows image data', async (): Promise => { + await compose.buildAll({ cwd: path.join(__dirname), log: logOutput }) + + const std = await compose.image.list({ + cwd: path.join(__dirname), + log: logOutput + }) + console.log(std.out) + + expect(std.err).toBeFalsy() + expect(std.data.services.length).toBe(3) + const web = std.data.services.find( + (service) => service.container === 'compose_test_web' + ) + expect(web).toBeDefined() + expect(web?.repository).toBe('nginx') + expect(web?.tag).toBe('1.16.0') + expect(web?.id).toBeTruthy() + expect(web?.id).toMatch(/^\w{12}$/) + + const hello = std.data.services.find( + (service) => service.container === 'compose_test_hello' + ) + expect(hello).toBeDefined() + expect(hello?.repository).toBe('hello-world') + expect(hello?.tag).toBe('latest') + expect(hello?.id).toMatch(/^\w{12}$/) + }) + + it('image list shows image data using json format', async (): Promise => { + await compose.buildAll({ cwd: path.join(__dirname), log: logOutput }) + + const std = await compose.image.list({ + cwd: path.join(__dirname), + log: logOutput, + commandOptions: [['--format', 'json']] + }) + + expect(std.err).toBeFalsy() + expect(std.data.services.length).toBe(3) + + const web = std.data.services.find( + (service) => service.container === 'compose_test_web' + ) + expect(web).toBeDefined() + expect(web?.repository).toBe('nginx') + expect(web?.tag).toBe('1.16.0') + expect(web?.id).toMatch(/^\w{12}$/) + + const hello = std.data.services.find( + (service) => service.container === 'compose_test_hello' + ) + expect(hello).toBeDefined() + expect(hello?.repository).toBe('hello-world') + expect(hello?.tag).toBe('latest') + expect(hello?.id).toMatch(/^\w{12}$/) + }) +}) + describe('when restarting all containers', (): void => { it('all containers restart', async (): Promise => { await compose.upAll({ cwd: path.join(__dirname), log: logOutput }) @@ -966,6 +1026,69 @@ hello }) }) +describe('parseImListOutput', (): void => { + it('parses image list output', () => { + // eslint-disable-next-line no-useless-escape + const output = + 'CONTAINER REPOSITORY TAG IMAGE ID SIZE\ncompose_test_hello hello-world latest d2c94e258dcb 13.3kB\ncompose_test_proxy nginx 1.19.9-alpine 72ab4137bd85 22.6MB\ncompose_test_web nginx 1.16.0 ae893c58d83f 109MB\n' + + const psOut = mapImListOutput(output) + + // prettier-ignore + expect(psOut.services[0]).toEqual({ + container: 'compose_test_hello', + repository: 'hello-world', + tag: 'latest', + id: 'd2c94e258dcb' + }) + + // prettier-ignore + expect(psOut.services[1]).toEqual({ + container: 'compose_test_proxy', + repository: 'nginx', + tag: '1.19.9-alpine', + id: '72ab4137bd85' + }) + + expect(psOut.services[2]).toEqual({ + container: 'compose_test_web', + repository: 'nginx', + tag: '1.16.0', + id: 'ae893c58d83f' + }) + }) +}) + +describe('image list command in quiet mode', (): void => { + it('image list returns container ids when quiet', () => { + const output = + '72ab4137bd85aae7970407cbf4ba98ec0a7cb9d302e93a38bb665ba5fddf6f5d\nae893c58d83fe2bd391fbec97f5576c9a34fea55b4ee9daf15feb9620b14b226\nd2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a\n' + + const psOut = mapImListOutput(output, { commandOptions: ['-q'] }) + + expect(psOut.services[0]).toEqual( + expect.objectContaining({ + container: + '72ab4137bd85aae7970407cbf4ba98ec0a7cb9d302e93a38bb665ba5fddf6f5d' + }) + ) + + expect(psOut.services[1]).toEqual( + expect.objectContaining({ + container: + 'ae893c58d83fe2bd391fbec97f5576c9a34fea55b4ee9daf15feb9620b14b226' + }) + ) + + expect(psOut.services[2]).toEqual( + expect.objectContaining({ + container: + 'd2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a' + }) + ) + }) +}) + describe('passed callback fn', (): void => { it('is called', async (): Promise => { const config = { From de6996d2f6fa5d615ab7c8a91683ad32b6fbac03 Mon Sep 17 00:00:00 2001 From: alex-Symbroson Date: Wed, 13 Mar 2024 14:29:57 +0100 Subject: [PATCH 3/5] added compose.create* commands --- src/v2.ts | 20 ++++++++++++++++++++ test/v2/compose.test.ts | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/v2.ts b/src/v2.ts index 1570138f..dbfd7f35 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -496,6 +496,26 @@ export const buildOne = function ( return execCompose('build', [service], options) } +export const createAll = function ( + options: IDockerComposeOptions = {} +): Promise { + return execCompose('create', [], options) +} + +export const createMany = function ( + services: string[], + options: IDockerComposeOptions = {} +): Promise { + return execCompose('create', services, options) +} + +export const createOne = function ( + service: string, + options?: IDockerComposeOptions +): Promise { + return execCompose('create', [service], options) +} + export const pullAll = function ( options: IDockerComposeOptions = {} ): Promise { diff --git a/test/v2/compose.test.ts b/test/v2/compose.test.ts index b759829b..11c0cb39 100644 --- a/test/v2/compose.test.ts +++ b/test/v2/compose.test.ts @@ -776,7 +776,7 @@ describe('when calling ps command', (): void => { describe('when calling image list command', (): void => { it('image list shows image data', async (): Promise => { - await compose.buildAll({ cwd: path.join(__dirname), log: logOutput }) + await compose.createAll({ cwd: path.join(__dirname), log: logOutput }) const std = await compose.image.list({ cwd: path.join(__dirname), @@ -805,7 +805,7 @@ describe('when calling image list command', (): void => { }) it('image list shows image data using json format', async (): Promise => { - await compose.buildAll({ cwd: path.join(__dirname), log: logOutput }) + await compose.createAll({ cwd: path.join(__dirname), log: logOutput }) const std = await compose.image.list({ cwd: path.join(__dirname), From 676757698e4093b65bec99020fdf11abffcb139b Mon Sep 17 00:00:00 2001 From: alex-Symbroson Date: Wed, 13 Mar 2024 14:34:18 +0100 Subject: [PATCH 4/5] renamed images command & added usage docs --- docs/api.md | 4 ++++ src/v2.ts | 24 +++++++++++------------- test/v2/compose.test.ts | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2c70a6a3..6b83453d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -16,9 +16,13 @@ This page demonstrates the usage of `docker-compose` for Node.js. * `config(options)` - Validates configuration files and returns configuration yaml * `configServices(options)` - Returns list of services defined in configuration files * `configVolumes(options)` - Returns list of volumes defined in configuration files +* `createAll(options)` - Create or recreate services +* `createMany(services, options)` - Create or recreate services +* `createOne(service, options)` - Create or recreate service * `down(options)` - Stops containers and removes containers, networks, volumes, and images created by `up` * `exec(container, command, options)` - Exec `command` inside `container` - uses `-T` to properly handle stdin & stdout * `kill(options)` - Force stop service containers +* `images(options)` - Show all created images * `logs(services, options)` - Show logs of service(s) - use `options.follow` `true|false` to turn on `--follow` flag * `pauseOne(service, options)` - Pause the specified service * `port(service, containerPort, options)` - Returns the public port of the given service and internal port. diff --git a/src/v2.ts b/src/v2.ts index dbfd7f35..68fd4d19 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -596,20 +596,18 @@ export const ps = async function ( } } -export const image = { - list: async function ( - options?: IDockerComposeOptions - ): Promise> { - try { - const result = await execCompose('images', [], options) - const data = mapImListOutput(result.out, options) - return { - ...result, - data - } - } catch (error) { - return Promise.reject(error) +export const images = async function ( + options?: IDockerComposeOptions +): Promise> { + try { + const result = await execCompose('images', [], options) + const data = mapImListOutput(result.out, options) + return { + ...result, + data } + } catch (error) { + return Promise.reject(error) } } diff --git a/test/v2/compose.test.ts b/test/v2/compose.test.ts index 11c0cb39..de2389ce 100644 --- a/test/v2/compose.test.ts +++ b/test/v2/compose.test.ts @@ -778,7 +778,7 @@ describe('when calling image list command', (): void => { it('image list shows image data', async (): Promise => { await compose.createAll({ cwd: path.join(__dirname), log: logOutput }) - const std = await compose.image.list({ + const std = await compose.images({ cwd: path.join(__dirname), log: logOutput }) @@ -807,7 +807,7 @@ describe('when calling image list command', (): void => { it('image list shows image data using json format', async (): Promise => { await compose.createAll({ cwd: path.join(__dirname), log: logOutput }) - const std = await compose.image.list({ + const std = await compose.images({ cwd: path.join(__dirname), log: logOutput, commandOptions: [['--format', 'json']] From 9b8167db1054004a182a741131037cb95aa21351 Mon Sep 17 00:00:00 2001 From: alex-Symbroson Date: Wed, 13 Mar 2024 14:48:21 +0100 Subject: [PATCH 5/5] fixed prefer-const --- src/v2.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v2.ts b/src/v2.ts index 68fd4d19..2d317ab0 100644 --- a/src/v2.ts +++ b/src/v2.ts @@ -201,10 +201,10 @@ export const mapImListOutput = ( // CONTAINER REPOSITORY TAG IMAGE ID SIZE const lineColumns = line.split(/\s{3,}/) - let containerFragment = lineColumns[0] || line - let repositoryFragment = lineColumns[1] || '' - let tagFragment = lineColumns[2] || '' - let idFragment = lineColumns[3] || '' + const containerFragment = lineColumns[0] || line + const repositoryFragment = lineColumns[1] || '' + const tagFragment = lineColumns[2] || '' + const idFragment = lineColumns[3] || '' return { container: containerFragment.trim(),