UNPKG

@sha1n/fungus

Version:

A dependency based service graph controller library

191 lines (162 loc) 5.35 kB
import { retryAround, RetryPolicy, simpleRetryPolicy, TimeUnit } from '@sha1n/about-time'; import child_process from 'child_process'; import { v4 as uuid } from 'uuid'; import { createLogger } from '../../lib/logger'; import { Service, ServiceMetadata } from '../../lib/types'; const exitEvents = ['SIGINT', 'SIGTERM', 'uncaughtException']; const logger = createLogger('docker-service'); type DockerContainerOptions = { image: string; name?: string; remove?: boolean; ports?: { [key: string]: string }; volumes?: { [key: string]: string }; env?: { [key: string]: string }; cmd?: string; network?: string; daemon?: boolean; healthCheck?: { check: () => Promise<void>; retryPolicy?: RetryPolicy; }; }; type DockerVolumeOptions = { name?: string; remove?: boolean; }; type ContainerMetadata = { name?: string; ports?: { [key: string]: string }; volumes?: { [key: string]: string }; network?: string; toString: () => string; } & ServiceMetadata; function createContainerService(opts: DockerContainerOptions): Service { const name = opts.name || uuid(); const exitHandler = async () => executeCommand(`docker rm -fv ${name}`); registerExitHandlers(exitHandler); let started = false; return { id: name, start: async (): Promise<ContainerMetadata> => { logger.debug('starting container %s...', name); await executeCommand(interpret(name, opts)); started = true; logger.debug('container %s started', name); if (opts?.healthCheck) { logger.info('waiting for container %s to pass health check...', name); await retryAround( async () => { logger.info('checking health of container %s...', name); await opts.healthCheck.check(); }, opts.healthCheck?.retryPolicy || simpleRetryPolicy(10, 1, { units: TimeUnit.Second }) ); logger.info('container %s is ready', name); } return { id: name, name, ports: opts.ports, volumes: opts.volumes, network: opts.network, toString: () => `[container:${name}]` }; }, stop: async () => { if (started) { logger.debug('stopping container %s...', name); await executeCommand(`docker stop ${name}`).finally(() => removeExitHandlers(exitHandler)); logger.debug('container %s stopped', name); } } }; } function createVolumeService(opts?: DockerVolumeOptions): Service { const volumeName = opts?.name || uuid(); // This won't work if a container is attached to the volume const exitHandler = async () => executeCommand(`docker volume remove --force ${volumeName}`); registerExitHandlers(exitHandler); let started = false; return { id: volumeName, start: async (): Promise<ContainerMetadata> => { logger.debug('creating docker volume %s...', volumeName); await executeCommand(`docker volume create ${volumeName}`); started = true; logger.debug('volume %s started', volumeName); return { id: volumeName, toString: () => `[volume:${volumeName}]` }; }, stop: async () => { if (started && opts?.remove) { logger.debug('deleting volume %s...', volumeName); await exitHandler().finally(() => removeExitHandlers(exitHandler)); logger.debug('volume %s removed', volumeName); } } }; } function interpret(name: string, opts: DockerContainerOptions): string { const command = ['docker', 'run', '--name', name]; if (opts.daemon) { command.push('-d'); } if (opts.remove) { command.push('--rm'); } if (opts.network) { command.push('--network', opts.network); } const appendArgs = (flag: string, index: { [key: string]: string }, delim: string) => { if (index) { for (const key of Object.keys(index)) { command.push(flag, `${key}${delim}${index[key]}`); } } }; appendArgs('-e', opts.env, '='); appendArgs('-v', opts.volumes, ':'); appendArgs('-p', opts.ports, ':'); command.push(opts.image); return command.join(' '); } async function dockerExec(container: string, command: string): Promise<number> { const dockerCommand = `docker exec ${container} ${command}`; return executeCommand(dockerCommand); } async function executeCommand(cmd: string): Promise<number> { logger.debug('running: %s', cmd); return new Promise<number>((resolve, reject) => { const p = child_process.spawn(cmd, { stdio: 'inherit', shell: true }); const onExit = (code: number) => { if (code !== 0) { reject(new Error(`Command exit code: ${code}`)); } resolve(code); }; p.on('error', reject); p.on('exit', onExit); p.on('close', onExit); }); } function registerExitHandlers<T>(...handlers: Array<() => Promise<T>>): void { handlers.forEach(h => { exitEvents.forEach(e => { process.on(e, async () => { await h().catch(logger.error); }); }); }); } function removeExitHandlers<T>(...handlers: Array<() => Promise<T>>): void { handlers.forEach(h => { exitEvents.forEach(e => { process.removeListener(e, h); }); }); } export { DockerContainerOptions, ContainerMetadata, DockerVolumeOptions, createVolumeService, dockerExec }; export default createContainerService;