/** * dockerode client: socket selection, the shared singleton, and the `safe()` * error-normalizing wrapper. Every runtime call in the plugin goes through the * `ContainerClient` returned by `getClient()`. * * Why a unix socket and not the podman/docker CLI: the plugin runs inside the * Signal K server container, where the host runtime socket is bind-mounted * (`/var/run/docker.sock` in the universal-installer topology). Talking the * Docker API directly over that socket removes the need for any runtime CLI * binary in the Signal K base image, and removes the whole class of * podman-docker-shim flag-validation workarounds the CLI path needed — the API * accepts podman-specific create fields (e.g. `HostConfig.UsernsMode: * "keep-id"`) with no client-side validation. * * Adapted from signalk-updater-server/src/podman/client.ts (the proven dockerode * wrapper shared across the engine containers). */ import Docker from "dockerode"; import { type CategorizedError } from "./errors.js"; /** * The dockerode surface the plugin actually uses. Production passes the real * `Docker` instance; tests pass a hand-rolled mock implementing only the * methods in play. This is the injection seam that replaced the CLI-era * `ExecFn` stub: instead of feeding canned `{stdout, exitCode}` strings, tests * stub typed dockerode methods and assert object-field access. */ export interface ContainerClient { getContainer(id: string): Docker.Container; getImage(name: string): Docker.Image; getNetwork(id: string): Docker.Network; createContainer(opts: Docker.ContainerCreateOptions): Promise; createNetwork(opts: Docker.NetworkCreateOptions): Promise; listContainers(opts?: Docker.ContainerListOptions): Promise; listImages(opts?: Docker.ListImagesOptions): Promise; pull(repoTag: string, opts?: object): Promise; pruneImages(opts?: object): Promise; version(): Promise; info(): Promise; /** docker-modem; typed `any` upstream. Used for followProgress + demuxStream. */ modem: { followProgress(stream: NodeJS.ReadableStream, onFinished: (error: Error | null, result: unknown[]) => void, onProgress?: (obj: unknown) => void): void; demuxStream(stream: NodeJS.ReadableStream, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream): void; }; } /** Runtime the caller prefers when more than one socket is reachable. */ export type SocketPreference = "auto" | "podman" | "docker"; export interface ResolvedClient { /** * The resolved dockerode client, typed as the narrow `ContainerClient` * surface the plugin uses (a real `Docker` instance satisfies it). Consumers * get a usable client with no `as` cast. */ client: ContainerClient; socketPath: string; } /** * Resolve (once) and cache the dockerode client + its socket path. Returns * `null` when no socket answers — the caller surfaces this as "no container * runtime" via the doctor. Re-resolves after `resetClient()`. */ export declare function resolveClient(preference?: SocketPreference): Promise; /** * The shared client for code paths that have already resolved a runtime (every * consumer call after `detectRuntime` succeeded). Throws if called before * resolution — that's a programming error, not a runtime condition. */ export declare function getClient(): ContainerClient; /** Path of the resolved socket, for doctor/diagnostic display. */ export declare function getSocketPath(): string | undefined; /** * Drop the cached client. Called from `plugin.stop()` so a stop/start cycle * re-probes the socket, and from tests to reset between cases. */ export declare function resetClient(): void; /** * Test-only: install a specific client + socket path as the resolved singleton, * bypassing socket probing. Pass `null` to clear. */ export declare function _setClientForTesting(client: ContainerClient | null, socketPath?: string): void; /** * Test-only: drive `pickSocket` against an explicit candidate list instead of * the hardcoded conventional paths, so the existing-but-refused fallback can be * exercised with a real (chmod-restricted) unix socket. Returns the resolved * socket path or `null`; the client is irrelevant to the fallback assertion. */ export declare function _pickSocketForTesting(candidates: string[]): Promise; /** * Run a dockerode op and normalize any throw into a categorized error. The one * place the `{stdout, stderr, exitCode}` CLI contract is replaced: callers * branch on `result.ok` instead of inspecting an exit code. */ export declare function safe(op: () => Promise): Promise<{ ok: true; value: T; } | { ok: false; error: CategorizedError; }>; /** * `inspect`-style convenience: returns `null` when the resource is absent (404) * and rethrows anything else. Replaces the CLI-era "exitCode !== 0 → missing" * idiom, which couldn't distinguish "not found" from "daemon broken". */ export declare function safeInspect(op: () => Promise): Promise; /** * Demultiplex a dockerode logs/exec stream into combined plain text. * * Containers started without a TTY (the dockerode default, and how every * managed container starts) deliver logs as a multiplexed stream: each * stdout/stderr chunk is prefixed with an 8-byte header * (`[stream-type, 0, 0, 0, len32]`). Reading the raw bytes as UTF-8 leaks those * headers into rendered log lines as binary garbage. `modem.demuxStream` splits * the framing into two writable sinks; we merge both into one text callback * because the plugin surfaces combined stdout+stderr (matching `podman logs` * semantics). The CLI used to pre-demux for us — over the API we must do it. * * Returns a function that, given a finished/destroyed source stream, has piped * all demuxed text into `onText`. Caller wires `onText` to a line splitter. */ export declare function demuxToText(modem: ContainerClient["modem"], source: NodeJS.ReadableStream, onText: (chunk: string) => void): void; /** * Read a finished (non-follow) logs/exec stream fully and return the demuxed * combined text. Used by one-shot `getContainerLogs` and `execInContainer`, * which under the CLI got pre-demuxed text and now must demux a buffer. */ export declare function demuxBufferToText(modem: ContainerClient["modem"], source: NodeJS.ReadableStream): Promise; //# sourceMappingURL=client.d.ts.map