import * as plugins from './plugins.js'; import * as interfaces from './interfaces/index.js'; import { DockerHost } from './classes.host.js'; import { DockerResource } from './classes.base.js'; import { logger } from './logger.js'; export class DockerContainer extends DockerResource { // STATIC (Internal - prefixed with _ to indicate internal use) /** * Internal: Get all containers * Public API: Use dockerHost.getContainers() instead */ public static async _list( dockerHostArg: DockerHost, ): Promise { const result: DockerContainer[] = []; const response = await dockerHostArg.request('GET', '/containers/json'); // TODO: Think about getting the config by inspecting the container for (const containerResult of response.body) { result.push(new DockerContainer(dockerHostArg, containerResult)); } return result; } /** * Internal: Get a container by ID * Public API: Use dockerHost.getContainerById(id) instead */ public static async _fromId( dockerHostArg: DockerHost, containerId: string, ): Promise { const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`); return new DockerContainer(dockerHostArg, response.body); } /** * Internal: Create a container * Public API: Use dockerHost.createContainer(descriptor) instead */ public static async _create( dockerHost: DockerHost, containerCreationDescriptor: interfaces.IContainerCreationDescriptor, ): Promise { // Check for unique hostname const existingContainers = await DockerContainer._list(dockerHost); const sameHostNameContainer = existingContainers.find((container) => { // TODO implement HostName Detection; return false; }); const response = await dockerHost.request('POST', '/containers/create', { Hostname: containerCreationDescriptor.Hostname, Domainname: containerCreationDescriptor.Domainname, User: 'root', }); if (response.statusCode < 300) { logger.log('info', 'Container created successfully'); // Return the created container instance return await DockerContainer._fromId(dockerHost, response.body.Id); } else { logger.log('error', 'There has been a problem when creating the container'); throw new Error(`Failed to create container: ${response.statusCode}`); } } // INSTANCE PROPERTIES public Id: string; public Names: string[]; public Image: string; public ImageID: string; public Command: string; public Created: number; public Ports: interfaces.TPorts; public Labels: interfaces.TLabels; public State: string; public Status: string; public HostConfig: any; public NetworkSettings: { Networks: { [key: string]: { IPAMConfig: any; Links: any; Aliases: any; NetworkID: string; EndpointID: string; Gateway: string; IPAddress: string; IPPrefixLen: number; IPv6Gateway: string; GlobalIPv6Address: string; GlobalIPv6PrefixLen: number; MacAddress: string; DriverOpts: any; }; }; }; public Mounts: any; constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) { super(dockerHostArg); Object.keys(dockerContainerObjectArg).forEach((keyArg) => { this[keyArg] = dockerContainerObjectArg[keyArg]; }); } // INSTANCE METHODS /** * Refreshes this container's state from the Docker daemon */ public async refresh(): Promise { const updated = await DockerContainer._fromId(this.dockerHost, this.Id); Object.assign(this, updated); } /** * Inspects the container and returns detailed information */ public async inspect(): Promise { const response = await this.dockerHost.request('GET', `/containers/${this.Id}/json`); // Update instance with fresh data Object.assign(this, response.body); return response.body; } /** * Starts the container */ public async start(): Promise { const response = await this.dockerHost.request('POST', `/containers/${this.Id}/start`); if (response.statusCode >= 300) { throw new Error(`Failed to start container: ${response.statusCode}`); } await this.refresh(); } /** * Stops the container * @param options Options for stopping (e.g., timeout in seconds) */ public async stop(options?: { t?: number }): Promise { const queryParams = options?.t ? `?t=${options.t}` : ''; const response = await this.dockerHost.request('POST', `/containers/${this.Id}/stop${queryParams}`); if (response.statusCode >= 300) { throw new Error(`Failed to stop container: ${response.statusCode}`); } await this.refresh(); } /** * Removes the container * @param options Options for removal (force, remove volumes, remove link) */ public async remove(options?: { force?: boolean; v?: boolean; link?: boolean }): Promise { const queryParams = new URLSearchParams(); if (options?.force) queryParams.append('force', '1'); if (options?.v) queryParams.append('v', '1'); if (options?.link) queryParams.append('link', '1'); const queryString = queryParams.toString(); const response = await this.dockerHost.request( 'DELETE', `/containers/${this.Id}${queryString ? '?' + queryString : ''}`, ); if (response.statusCode >= 300) { throw new Error(`Failed to remove container: ${response.statusCode}`); } } /** * Gets container logs * @param options Log options (stdout, stderr, timestamps, tail, since, follow) */ public async logs(options?: { stdout?: boolean; stderr?: boolean; timestamps?: boolean; tail?: number | 'all'; since?: number; follow?: boolean; }): Promise { const queryParams = new URLSearchParams(); queryParams.append('stdout', options?.stdout !== false ? '1' : '0'); queryParams.append('stderr', options?.stderr !== false ? '1' : '0'); if (options?.timestamps) queryParams.append('timestamps', '1'); if (options?.tail) queryParams.append('tail', options.tail.toString()); if (options?.since) queryParams.append('since', options.since.toString()); if (options?.follow) queryParams.append('follow', '1'); const response = await this.dockerHost.request('GET', `/containers/${this.Id}/logs?${queryParams.toString()}`); // Docker returns logs with a special format (8 bytes header + payload) // For simplicity, we'll return the raw body as string return response.body.toString(); } /** * Gets container stats * @param options Stats options (stream for continuous stats) */ public async stats(options?: { stream?: boolean }): Promise { const queryParams = new URLSearchParams(); queryParams.append('stream', options?.stream ? '1' : '0'); const response = await this.dockerHost.request('GET', `/containers/${this.Id}/stats?${queryParams.toString()}`); return response.body; } /** * Streams container logs continuously (follow mode) * Returns a readable stream that emits log data as it's produced * @param options Log streaming options */ public async streamLogs(options?: { stdout?: boolean; stderr?: boolean; timestamps?: boolean; tail?: number | 'all'; since?: number; }): Promise { const queryParams = new URLSearchParams(); queryParams.append('stdout', options?.stdout !== false ? '1' : '0'); queryParams.append('stderr', options?.stderr !== false ? '1' : '0'); queryParams.append('follow', '1'); // Always follow for streaming if (options?.timestamps) queryParams.append('timestamps', '1'); if (options?.tail) queryParams.append('tail', options.tail.toString()); if (options?.since) queryParams.append('since', options.since.toString()); const response = await this.dockerHost.requestStreaming( 'GET', `/containers/${this.Id}/logs?${queryParams.toString()}` ); // requestStreaming returns Node.js stream return response as plugins.smartstream.stream.Readable; } /** * Attaches to the container's main process (PID 1) * Returns a duplex stream for bidirectional communication * @param options Attach options */ public async attach(options?: { stream?: boolean; stdin?: boolean; stdout?: boolean; stderr?: boolean; logs?: boolean; }): Promise<{ stream: plugins.smartstream.stream.Duplex; close: () => Promise; }> { const queryParams = new URLSearchParams(); queryParams.append('stream', options?.stream !== false ? '1' : '0'); queryParams.append('stdin', options?.stdin ? '1' : '0'); queryParams.append('stdout', options?.stdout !== false ? '1' : '0'); queryParams.append('stderr', options?.stderr !== false ? '1' : '0'); if (options?.logs) queryParams.append('logs', '1'); const response = await this.dockerHost.requestStreaming( 'POST', `/containers/${this.Id}/attach?${queryParams.toString()}` ); // Create a duplex stream for bidirectional communication const nodeStream = response as plugins.smartstream.stream.Readable; // Convert to duplex by wrapping in SmartDuplex const duplexStream = new plugins.smartstream.SmartDuplex({ writeFunction: async (chunk) => { // Write data is sent to the container's stdin return chunk; }, readableObjectMode: false, writableObjectMode: false, }); // Pipe container output to our duplex readable side nodeStream.on('data', (chunk) => { duplexStream.push(chunk); }); nodeStream.on('end', () => { duplexStream.push(null); // Signal end of stream }); nodeStream.on('error', (error) => { duplexStream.destroy(error); }); // Helper function to close the attachment const close = async () => { duplexStream.end(); if (nodeStream.destroy) { nodeStream.destroy(); } }; return { stream: duplexStream, close, }; } /** * Executes a command in the container * Returns a duplex stream for command interaction * @param command Command to execute (string or array of strings) * @param options Exec options */ public async exec( command: string | string[], options?: { tty?: boolean; attachStdin?: boolean; attachStdout?: boolean; attachStderr?: boolean; env?: string[]; workingDir?: string; user?: string; } ): Promise<{ stream: plugins.smartstream.stream.Duplex; close: () => Promise; }> { // Step 1: Create exec instance const createResponse = await this.dockerHost.request('POST', `/containers/${this.Id}/exec`, { Cmd: typeof command === 'string' ? ['/bin/sh', '-c', command] : command, AttachStdin: options?.attachStdin !== false, AttachStdout: options?.attachStdout !== false, AttachStderr: options?.attachStderr !== false, Tty: options?.tty || false, Env: options?.env || [], WorkingDir: options?.workingDir, User: options?.user, }); const execId = createResponse.body.Id; // Step 2: Start exec instance with streaming response const startResponse = await this.dockerHost.requestStreaming( 'POST', `/exec/${execId}/start`, undefined, // no stream input { Detach: false, Tty: options?.tty || false, } ); const nodeStream = startResponse as plugins.smartstream.stream.Readable; // Create duplex stream for bidirectional communication const duplexStream = new plugins.smartstream.SmartDuplex({ writeFunction: async (chunk) => { return chunk; }, readableObjectMode: false, writableObjectMode: false, }); // Pipe exec output to duplex readable side nodeStream.on('data', (chunk) => { duplexStream.push(chunk); }); nodeStream.on('end', () => { duplexStream.push(null); }); nodeStream.on('error', (error) => { duplexStream.destroy(error); }); const close = async () => { duplexStream.end(); if (nodeStream.destroy) { nodeStream.destroy(); } }; return { stream: duplexStream, close, }; } }