Skip to content

Commit

Permalink
Merge pull request #268 from alex-Symbroson/master
Browse files Browse the repository at this point in the history
images & create* commands
  • Loading branch information
AlexZeitler authored Mar 13, 2024
2 parents 6275845 + 9b8167d commit 4a0069e
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 3 deletions.
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
103 changes: 103 additions & 0 deletions src/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DockerComposePsResultService>
}

export type DockerComposeImListResult = {
services: Array<DockerComposeImListResultService>
}

const arrayIncludesTuple = (
arr: string[] | (string | string[])[],
tuple: string[]
Expand Down Expand Up @@ -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,}/)

const containerFragment = lineColumns[0] || line
const repositoryFragment = lineColumns[1] || ''
const tagFragment = lineColumns[2] || ''
const 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
Expand Down Expand Up @@ -428,6 +496,26 @@ export const buildOne = function (
return execCompose('build', [service], options)
}

export const createAll = function (
options: IDockerComposeOptions = {}
): Promise<IDockerComposeResult> {
return execCompose('create', [], options)
}

export const createMany = function (
services: string[],
options: IDockerComposeOptions = {}
): Promise<IDockerComposeResult> {
return execCompose('create', services, options)
}

export const createOne = function (
service: string,
options?: IDockerComposeOptions
): Promise<IDockerComposeResult> {
return execCompose('create', [service], options)
}

export const pullAll = function (
options: IDockerComposeOptions = {}
): Promise<IDockerComposeResult> {
Expand Down Expand Up @@ -508,6 +596,21 @@ export const ps = async function (
}
}

export const images = async function (
options?: IDockerComposeOptions
): Promise<TypedDockerComposeResult<DockerComposeImListResult>> {
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<IDockerComposeResult> {
Expand Down
2 changes: 1 addition & 1 deletion test/v1/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ test('removes container', async (): Promise<void> => {
test('returns version information', async (): Promise<void> => {
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', () => {
Expand Down
127 changes: 125 additions & 2 deletions test/v2/compose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> =>
Expand Down Expand Up @@ -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<void> => {
await compose.createAll({ cwd: path.join(__dirname), log: logOutput })

const std = await compose.images({
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<void> => {
await compose.createAll({ cwd: path.join(__dirname), log: logOutput })

const std = await compose.images({
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<void> => {
await compose.upAll({ cwd: path.join(__dirname), log: logOutput })
Expand Down Expand Up @@ -866,7 +926,7 @@ describe('version command', (): void => {
it('returns version information', async (): Promise<void> => {
const version = (await compose.version()).data.version

expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)?(\+.*)*$/)
expect(version).toMatch(/^(\d+\.)?(\d+\.)?(\*|\d+)?(\+.*)*(-\w+(\.\d+))?$/)
})
})

Expand Down Expand Up @@ -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<void> => {
const config = {
Expand Down

0 comments on commit 4a0069e

Please sign in to comment.