Files
docker/ts/classes.container.ts

402 lines
12 KiB
TypeScript

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.listContainers() instead
*/
public static async _list(
dockerHostArg: DockerHost,
): Promise<DockerContainer[]> {
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
* Returns undefined if container does not exist
*/
public static async _fromId(
dockerHostArg: DockerHost,
containerId: string,
): Promise<DockerContainer | undefined> {
const containers = await this._list(dockerHostArg);
return containers.find((container) => container.Id === containerId);
}
/**
* Internal: Create a container
* Public API: Use dockerHost.createContainer(descriptor) instead
*/
public static async _create(
dockerHost: DockerHost,
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
): Promise<DockerContainer> {
// 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<void> {
const updated = await DockerContainer._fromId(this.dockerHost, this.Id);
Object.assign(this, updated);
}
/**
* Inspects the container and returns detailed information
*/
public async inspect(): Promise<any> {
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<void> {
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<void> {
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<void> {
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<string> {
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<any> {
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<plugins.smartstream.stream.Readable> {
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<void>;
}> {
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<void>;
inspect: () => Promise<interfaces.IExecInspectInfo>;
}> {
// 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();
}
};
const inspect = async (): Promise<interfaces.IExecInspectInfo> => {
const inspectResponse = await this.dockerHost.request('GET', `/exec/${execId}/json`);
return inspectResponse.body;
};
return {
stream: duplexStream,
close,
inspect,
};
}
}