153 lines
4.8 KiB
TypeScript
153 lines
4.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import Docker from "dockerode";
|
|
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
|
|
|
import { env } from "./env";
|
|
import type { DockerEndpointCapability, DockerEndpointDescriptor } from "./endpoint-descriptor";
|
|
import {
|
|
createLegacySocketDescriptor,
|
|
createLegacyTcpDescriptor,
|
|
getDockerEndpointHost,
|
|
parseDockerEndpointDescriptors,
|
|
} from "./endpoint-descriptor";
|
|
|
|
export interface DockerInstance {
|
|
endpointId: string;
|
|
endpointName: string;
|
|
host: string;
|
|
descriptor: DockerEndpointDescriptor;
|
|
instance: Docker;
|
|
}
|
|
|
|
export interface DockerEndpointInitializationFailure {
|
|
descriptor: DockerEndpointDescriptor;
|
|
host: string;
|
|
}
|
|
|
|
const logger = createLogger({ module: "dockerSingleton" });
|
|
|
|
export class DockerSingleton {
|
|
private static instances: DockerInstance[] | null = null;
|
|
private static initializationFailures: DockerEndpointInitializationFailure[] = [];
|
|
|
|
private createInstanceSafely(descriptor: DockerEndpointDescriptor): DockerInstance[] {
|
|
try {
|
|
return [createInstance(descriptor)];
|
|
} catch (error) {
|
|
const host = getDockerEndpointHost(descriptor);
|
|
DockerSingleton.initializationFailures.push({ descriptor, host });
|
|
logger.warn(
|
|
new ErrorWithMetadata(
|
|
"Failed to initialize Docker endpoint",
|
|
{ endpointId: descriptor.id, host },
|
|
{ cause: error },
|
|
),
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private createInstances() {
|
|
if (env.DOCKER_ENDPOINTS) {
|
|
return parseDockerEndpointDescriptors(env.DOCKER_ENDPOINTS).flatMap((descriptor) =>
|
|
this.createInstanceSafely(descriptor),
|
|
);
|
|
}
|
|
|
|
const socketPaths = env.DOCKER_SOCKET_PATHS;
|
|
const hostVariable = env.DOCKER_HOSTNAMES;
|
|
const portVariable = env.DOCKER_PORTS;
|
|
|
|
// Socket instances from DOCKER_SOCKET_PATHS
|
|
const socketInstances: DockerInstance[] = socketPaths
|
|
? socketPaths
|
|
.split(",")
|
|
.flatMap((socketPath) => this.createInstanceSafely(createLegacySocketDescriptor(socketPath)))
|
|
: [];
|
|
|
|
// TCP instances from existing DOCKER_HOSTNAMES/DOCKER_PORTS
|
|
let tcpInstances: DockerInstance[] = [];
|
|
if (hostVariable !== undefined && portVariable !== undefined) {
|
|
const hostnames = hostVariable.split(",");
|
|
const ports = portVariable.split(",");
|
|
|
|
if (hostnames.length !== ports.length) {
|
|
throw new Error("The number of hosts and ports must match");
|
|
}
|
|
|
|
tcpInstances = hostnames.flatMap((host, i) =>
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this.createInstanceSafely(createLegacyTcpDescriptor(host, parseInt(ports[i]!, 10))),
|
|
);
|
|
}
|
|
|
|
const instances = [...socketInstances, ...tcpInstances];
|
|
|
|
// Default: local socket if nothing else configured
|
|
if (instances.length === 0) {
|
|
const descriptor = {
|
|
...createLegacySocketDescriptor("/var/run/docker.sock"),
|
|
id: "socket:default",
|
|
name: "Local Docker",
|
|
source: "default" as const,
|
|
};
|
|
return [createInstance(descriptor)];
|
|
}
|
|
|
|
return instances;
|
|
}
|
|
|
|
public static findInstance(endpointId: string): DockerInstance | undefined {
|
|
return this.getInstances().find((instance) => instance.endpointId === endpointId);
|
|
}
|
|
|
|
public static hasCapability(endpointId: string, capability: DockerEndpointCapability): boolean {
|
|
return this.findInstance(endpointId)?.descriptor.capabilities.includes(capability) ?? false;
|
|
}
|
|
|
|
public static getInstances(): DockerInstance[] {
|
|
if (this.instances) {
|
|
return this.instances;
|
|
}
|
|
|
|
this.initializationFailures = [];
|
|
this.instances = new DockerSingleton().createInstances();
|
|
return this.instances;
|
|
}
|
|
|
|
public static getInitializationFailures(): DockerEndpointInitializationFailure[] {
|
|
this.getInstances();
|
|
return [...this.initializationFailures];
|
|
}
|
|
|
|
public static reset(): void {
|
|
this.instances = null;
|
|
this.initializationFailures = [];
|
|
}
|
|
}
|
|
|
|
const createInstance = (descriptor: DockerEndpointDescriptor): DockerInstance => ({
|
|
endpointId: descriptor.id,
|
|
endpointName: descriptor.name,
|
|
host: getDockerEndpointHost(descriptor),
|
|
descriptor,
|
|
instance: new Docker(createDockerOptions(descriptor)),
|
|
});
|
|
|
|
const createDockerOptions = (descriptor: DockerEndpointDescriptor) => {
|
|
const transport = descriptor.transport;
|
|
if (transport.type === "socket") return { socketPath: transport.path };
|
|
if (transport.type === "tcp") return { host: transport.host, port: transport.port, protocol: "http" as const };
|
|
return {
|
|
host: transport.host,
|
|
port: transport.port,
|
|
protocol: "https" as const,
|
|
ca: readFileSync(transport.caPath),
|
|
...(transport.certPath && transport.keyPath
|
|
? { cert: readFileSync(transport.certPath), key: readFileSync(transport.keyPath) }
|
|
: {}),
|
|
};
|
|
};
|