402 lines
12 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|