Files
homarr/packages/docker/src/singleton.ts
T

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) }
: {}),
};
};