mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-02 23:19:33 +00:00
165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
import Docker from 'dockerode';
|
|
import { DatabaseService, Node } from './DatabaseService';
|
|
|
|
/**
|
|
* NodeRegistry: Manages Docker daemon connections for multiple nodes.
|
|
* Replaces the old singleton DockerController pattern. Each node
|
|
* (local or remote) gets its own dedicated Docker client instance.
|
|
*/
|
|
export class NodeRegistry {
|
|
private static instance: NodeRegistry;
|
|
private connections: Map<number, Docker> = new Map();
|
|
|
|
private constructor() {}
|
|
|
|
public static getInstance(): NodeRegistry {
|
|
if (!NodeRegistry.instance) {
|
|
NodeRegistry.instance = new NodeRegistry();
|
|
}
|
|
return NodeRegistry.instance;
|
|
}
|
|
|
|
/**
|
|
* Get a Docker client for a specific node.
|
|
* Creates the connection lazily on first request and caches it.
|
|
*/
|
|
public getDocker(nodeId: number): Docker {
|
|
// Return cached connection if available
|
|
if (this.connections.has(nodeId)) {
|
|
return this.connections.get(nodeId)!;
|
|
}
|
|
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
|
|
if (!node) {
|
|
throw new Error(`Node with id ${nodeId} not found`);
|
|
}
|
|
|
|
const docker = this.createDockerClient(node);
|
|
this.connections.set(nodeId, docker);
|
|
return docker;
|
|
}
|
|
|
|
/**
|
|
* Get the Docker client for the default node.
|
|
* This is the backward-compatible path for all existing code.
|
|
*/
|
|
public getDefaultDocker(): Docker {
|
|
const db = DatabaseService.getInstance();
|
|
const defaultNode = db.getDefaultNode();
|
|
|
|
if (!defaultNode || !defaultNode.id) {
|
|
// Absolute fallback: local socket (preserves legacy behavior)
|
|
return new Docker();
|
|
}
|
|
|
|
return this.getDocker(defaultNode.id);
|
|
}
|
|
|
|
/**
|
|
* Get the default node ID. Returns the ID of the node marked as default.
|
|
*/
|
|
public getDefaultNodeId(): number {
|
|
const db = DatabaseService.getInstance();
|
|
const defaultNode = db.getDefaultNode();
|
|
return defaultNode?.id || 1;
|
|
}
|
|
|
|
/**
|
|
* Get a node configuration by its ID.
|
|
*/
|
|
public getNode(nodeId: number): Node | undefined {
|
|
const db = DatabaseService.getInstance();
|
|
return db.getNode(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Create a Docker client based on node configuration.
|
|
* - Local nodes: use the default socket (Docker autodetects)
|
|
* - Remote nodes: connect via TCP to host:port
|
|
*/
|
|
private createDockerClient(node: Node): Docker {
|
|
if (node.type === 'local') {
|
|
// Local node: use the default Docker socket
|
|
return new Docker();
|
|
}
|
|
|
|
// Remote node: connect via Docker TCP API
|
|
if (!node.host) {
|
|
throw new Error(`Remote node "${node.name}" is missing a host address`);
|
|
}
|
|
|
|
return new Docker({
|
|
host: node.host,
|
|
port: node.port || 2375,
|
|
// TODO: Phase 55.2 — Add TLS certificate support for secure remote connections
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test connectivity to a specific node.
|
|
* Returns true if we can ping the Docker daemon.
|
|
*/
|
|
public async testConnection(nodeId: number): Promise<{ success: boolean; error?: string; info?: any }> {
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
|
|
if (!node) {
|
|
return { success: false, error: 'Node not found' };
|
|
}
|
|
|
|
try {
|
|
const docker = this.createDockerClient(node);
|
|
const info = await docker.info();
|
|
|
|
// Validate the payload contains actual Docker daemon info, not arbitrary HTML
|
|
if (!info || !info.OperatingSystem || typeof info.Containers !== 'number') {
|
|
throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?");
|
|
}
|
|
|
|
db.updateNodeStatus(nodeId, 'online');
|
|
return {
|
|
success: true,
|
|
info: {
|
|
name: info.Name,
|
|
serverVersion: info.ServerVersion,
|
|
os: info.OperatingSystem,
|
|
architecture: info.Architecture,
|
|
containers: info.Containers,
|
|
containersRunning: info.ContainersRunning,
|
|
images: info.Images,
|
|
memTotal: info.MemTotal,
|
|
cpus: info.NCPU,
|
|
}
|
|
};
|
|
} catch (error: any) {
|
|
db.updateNodeStatus(nodeId, 'offline');
|
|
return { success: false, error: error.message || 'Connection failed' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Evict a cached connection (e.g., after node config changes).
|
|
*/
|
|
public evictConnection(nodeId: number): void {
|
|
this.connections.delete(nodeId);
|
|
}
|
|
|
|
/**
|
|
* Flush all cached connections.
|
|
*/
|
|
public flushAll(): void {
|
|
this.connections.clear();
|
|
}
|
|
|
|
/**
|
|
* Get the compose directory for a specific node.
|
|
*/
|
|
public getComposeDir(nodeId: number): string {
|
|
const db = DatabaseService.getInstance();
|
|
const node = db.getNode(nodeId);
|
|
return node?.compose_dir || process.env.COMPOSE_DIR || '/app/compose';
|
|
}
|
|
}
|