Files
NetMesh/domain/systemManager/inspectView.ts

188 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
* View models for `docker inspect` payloads (the summarized objects produced
* by electron/bridges/systemManager/dockerOps.cjs). All fields are optional
* the raw payload shape varies across docker versions, so every accessor is
* defensive.
*/
export interface ContainerInspectView {
id?: string;
image?: string;
status?: string;
startedAt?: string;
createdAt?: string;
restartPolicy?: string;
command?: string;
ports: string[];
networks: string[];
mounts: string[];
env: string[];
labels: string[];
}
export interface ImageInspectView {
id?: string;
tags: string[];
digests: string[];
createdAt?: string;
size?: string;
platform?: string;
entrypoint?: string;
cmd?: string;
workdir?: string;
exposedPorts: string[];
env: string[];
labels: string[];
}
type Dict = Record<string, unknown>;
function str(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value : undefined;
}
function rec(value: unknown): Dict | undefined {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Dict)
: undefined;
}
function strArray(value: unknown): string[] {
return Array.isArray(value) ? value.map((v) => String(v)).filter(Boolean) : [];
}
export function shortDockerId(value: unknown): string | undefined {
const raw = str(value);
if (!raw) return undefined;
return raw.replace(/^sha256:/, '').slice(0, 12);
}
function formatIsoDate(value: unknown): string | undefined {
const raw = str(value);
if (!raw) return undefined;
const date = new Date(raw);
if (Number.isNaN(date.getTime())) return raw;
// Docker uses 0001-01-01T00:00:00Z for "never".
if (date.getTime() <= 0) return undefined;
return date.toLocaleString();
}
export function formatBytes(value: unknown): string | undefined {
const num = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(num) || num < 0) return undefined;
if (num >= 1024 ** 3) return `${(num / 1024 ** 3).toFixed(2)} GB`;
if (num >= 1024 ** 2) return `${(num / 1024 ** 2).toFixed(1)} MB`;
if (num >= 1024) return `${(num / 1024).toFixed(1)} KB`;
return `${num} B`;
}
function labelLines(value: unknown): string[] {
const labels = rec(value);
if (!labels) return [];
return Object.entries(labels).map(([key, val]) => `${key}=${String(val ?? '')}`);
}
/** "0.0.0.0:8080->80/tcp" for published ports, "80/tcp" for exposed-only. */
function portLines(portsMap: unknown): string[] {
const ports = rec(portsMap);
if (!ports) return [];
const lines: string[] = [];
for (const [containerPort, bindings] of Object.entries(ports)) {
if (!Array.isArray(bindings) || bindings.length === 0) {
lines.push(containerPort);
continue;
}
for (const binding of bindings) {
const bind = rec(binding);
const hostIp = str(bind?.HostIp) ?? '0.0.0.0';
const hostPort = str(bind?.HostPort);
lines.push(hostPort ? `${hostIp}:${hostPort} -> ${containerPort}` : containerPort);
}
}
return [...new Set(lines)];
}
function commandLine(path: unknown, args: unknown): string | undefined {
const parts = [str(path), ...strArray(args)].filter(Boolean) as string[];
return parts.length ? parts.join(' ') : undefined;
}
function joinCommand(value: unknown): string | undefined {
if (Array.isArray(value)) {
const joined = value.map((v) => String(v)).join(' ').trim();
return joined || undefined;
}
return str(value);
}
export function buildContainerInspectView(data: Dict): ContainerInspectView {
const state = rec(data.state);
const network = rec(data.network);
let status = str(state?.Status);
const exitCode = typeof state?.ExitCode === 'number' ? state.ExitCode : undefined;
if (status && status !== 'running' && exitCode !== undefined && exitCode !== 0) {
status = `${status} (exit ${exitCode})`;
}
const networks: string[] = [];
const networksMap = rec(network?.Networks);
if (networksMap) {
for (const [name, info] of Object.entries(networksMap)) {
const ip = str(rec(info)?.IPAddress);
networks.push(ip ? `${name} · ${ip}` : name);
}
} else {
const ip = str(network?.IPAddress);
if (ip) networks.push(ip);
}
const mounts = Array.isArray(data.mounts)
? data.mounts.map((mount) => {
const m = rec(mount);
const source = str(m?.Source) ?? str(m?.Name) ?? str(m?.Type) ?? '?';
const destination = str(m?.Destination) ?? '?';
const mode = m?.RW === false ? 'ro' : 'rw';
return `${source} -> ${destination} (${mode})`;
})
: [];
const restartPolicy = str(rec(data.restartPolicy)?.Name);
return {
id: shortDockerId(data.id),
image: str(data.image),
status,
startedAt: formatIsoDate(state?.StartedAt),
createdAt: formatIsoDate(data.created),
restartPolicy,
command: commandLine(data.path, data.args),
ports: portLines(network?.Ports),
networks,
mounts,
env: strArray(data.env),
labels: labelLines(data.labels),
};
}
export function buildImageInspectView(data: Dict): ImageInspectView {
const config = rec(data.config);
const os = str(data.os);
const arch = str(data.architecture);
return {
id: shortDockerId(data.id),
tags: strArray(data.repoTags),
digests: strArray(data.repoDigests),
createdAt: formatIsoDate(data.created),
size: formatBytes(data.size),
platform: os && arch ? `${os}/${arch}` : os ?? arch,
entrypoint: joinCommand(config?.entrypoint),
cmd: joinCommand(config?.cmd),
workdir: str(config?.workingDir),
exposedPorts: Object.keys(rec(config?.exposedPorts) ?? {}),
env: strArray(config?.env),
labels: labelLines(config?.labels),
};
}