BREAKING CHANGE(DockerHost): Refactor public API to DockerHost facade; introduce DockerResource base; make resource static methods internal; support flexible descriptors and stream compatibility
This commit is contained in:
@@ -2,21 +2,23 @@ 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 {
|
||||
// STATIC
|
||||
export class DockerContainer extends DockerResource {
|
||||
// STATIC (Internal - prefixed with _ to indicate internal use)
|
||||
|
||||
/**
|
||||
* get all containers
|
||||
* Internal: Get all containers
|
||||
* Public API: Use dockerHost.getContainers() instead
|
||||
*/
|
||||
public static async getContainers(
|
||||
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 inpsecting the container
|
||||
// TODO: Think about getting the config by inspecting the container
|
||||
for (const containerResult of response.body) {
|
||||
result.push(new DockerContainer(dockerHostArg, containerResult));
|
||||
}
|
||||
@@ -24,46 +26,49 @@ export class DockerContainer {
|
||||
}
|
||||
|
||||
/**
|
||||
* gets an container by Id
|
||||
* @param containerId
|
||||
* Internal: Get a container by ID
|
||||
* Public API: Use dockerHost.getContainerById(id) instead
|
||||
*/
|
||||
public static async getContainerById(containerId: string) {
|
||||
// TODO: implement get container by id
|
||||
public static async _fromId(
|
||||
dockerHostArg: DockerHost,
|
||||
containerId: string,
|
||||
): Promise<DockerContainer> {
|
||||
const response = await dockerHostArg.request('GET', `/containers/${containerId}/json`);
|
||||
return new DockerContainer(dockerHostArg, response.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a container
|
||||
* Internal: Create a container
|
||||
* Public API: Use dockerHost.createContainer(descriptor) instead
|
||||
*/
|
||||
public static async create(
|
||||
public static async _create(
|
||||
dockerHost: DockerHost,
|
||||
containerCreationDescriptor: interfaces.IContainerCreationDescriptor,
|
||||
) {
|
||||
// check for unique hostname
|
||||
const existingContainers = await DockerContainer.getContainers(dockerHost);
|
||||
): 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',
|
||||
);
|
||||
logger.log('error', 'There has been a problem when creating the container');
|
||||
throw new Error(`Failed to create container: ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
// references
|
||||
public dockerHost: DockerHost;
|
||||
|
||||
// properties
|
||||
// INSTANCE PROPERTIES
|
||||
public Id: string;
|
||||
public Names: string[];
|
||||
public Image: string;
|
||||
@@ -95,10 +100,294 @@ export class DockerContainer {
|
||||
};
|
||||
};
|
||||
public Mounts: any;
|
||||
|
||||
constructor(dockerHostArg: DockerHost, dockerContainerObjectArg: any) {
|
||||
this.dockerHost = dockerHostArg;
|
||||
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>;
|
||||
}> {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user