@wpelevator/tug 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,359 @@
1
+ // Turns the desired state for one node into Engine API container definitions. Pure: no Docker, no
2
+ // filesystem. The config hash decides whether a running container already matches its definition.
3
+ import crypto from 'node:crypto';
4
+ import { bindPorts, crossNodeHosts } from "./compose.js";
5
+ import { sentinelNames, substituteSentinels } from "./normalize.js";
6
+ export const LABEL_PROJECT = 'tug.project';
7
+ export const LABEL_SERVICE = 'tug.service';
8
+ export const LABEL_REVISION = 'tug.revision';
9
+ export const LABEL_HASH = 'tug.config-hash';
10
+ /** The project network every container of the project joins; service names are its aliases. */
11
+ export const networkName = (project) => `tug_${project}`;
12
+ /** Docker volume name for a Compose volume; `external` volumes keep their own name. */
13
+ export const volumeName = (project, volume) => `tug_${project}_${volume}`;
14
+ export const containerName = (project, service, hash) => `tug-${project}-${service}-${hash.slice(0, 8)}`;
15
+ const SECOND_NS = 1_000_000_000;
16
+ const UNITS = {
17
+ ns: 1e-6,
18
+ us: 1e-3,
19
+ µs: 1e-3,
20
+ ms: 1,
21
+ s: 1000,
22
+ m: 60000,
23
+ h: 3600000,
24
+ };
25
+ /** Compose durations (`30s`, `1m30s`, `500ms`) or a number of seconds, in milliseconds. */
26
+ export function composeDurationMs(value) {
27
+ if (typeof value === 'number')
28
+ return value * 1000;
29
+ if (typeof value !== 'string' || !value)
30
+ return undefined;
31
+ let total = 0;
32
+ let rest = value;
33
+ while (rest) {
34
+ const m = rest.match(/^(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/);
35
+ if (!m)
36
+ return undefined;
37
+ total += Number(m[1]) * UNITS[m[2]];
38
+ rest = rest.slice(m[0].length);
39
+ }
40
+ return total;
41
+ }
42
+ const toNs = (value) => {
43
+ const ms = composeDurationMs(value);
44
+ return ms === undefined ? undefined : Math.round(ms * 1e6);
45
+ };
46
+ /** Compose sizes: bytes as a number, or `64m` / `1g` style strings. */
47
+ export function sizeBytes(value) {
48
+ if (typeof value === 'number')
49
+ return value;
50
+ if (typeof value !== 'string')
51
+ return undefined;
52
+ const m = value
53
+ .trim()
54
+ .toLowerCase()
55
+ .match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/);
56
+ if (!m)
57
+ return undefined;
58
+ const power = { '': 0, k: 1, m: 2, g: 3, t: 4 }[m[2]];
59
+ return Math.round(Number(m[1]) * 1024 ** power);
60
+ }
61
+ const asList = (value) => Array.isArray(value) ? value.map(String) : value == null ? [] : [String(value)];
62
+ function extraHosts(value) {
63
+ if (!value)
64
+ return [];
65
+ return Array.isArray(value)
66
+ ? value.map((entry) => String(entry).replace('=', ':'))
67
+ : Object.entries(value).map(([name, ip]) => `${name}:${ip}`);
68
+ }
69
+ function restartPolicy(value) {
70
+ if (typeof value !== 'string' || !value)
71
+ return undefined;
72
+ const [name, max] = value.split(':');
73
+ return name === 'on-failure' && max
74
+ ? { Name: name, MaximumRetryCount: Number(max) }
75
+ : { Name: name === 'no' ? '' : name };
76
+ }
77
+ function healthcheck(value) {
78
+ if (!value || typeof value !== 'object')
79
+ return undefined;
80
+ const hc = value;
81
+ if (hc.disable === true)
82
+ return { Test: ['NONE'] };
83
+ const test = typeof hc.test === 'string' ? ['CMD-SHELL', hc.test] : hc.test;
84
+ return {
85
+ ...(test ? { Test: test } : {}),
86
+ ...(toNs(hc.interval) !== undefined ? { Interval: toNs(hc.interval) } : {}),
87
+ ...(toNs(hc.timeout) !== undefined ? { Timeout: toNs(hc.timeout) } : {}),
88
+ ...(toNs(hc.start_period) !== undefined ? { StartPeriod: toNs(hc.start_period) } : {}),
89
+ ...(toNs(hc.start_interval) !== undefined ? { StartInterval: toNs(hc.start_interval) } : {}),
90
+ ...(hc.retries !== undefined ? { Retries: Number(hc.retries) } : {}),
91
+ };
92
+ }
93
+ function portMaps(svc, ports) {
94
+ const exposed = {};
95
+ const bindings = {};
96
+ for (const entry of ports) {
97
+ const key = `${Number(entry.target)}/${entry.protocol || 'tcp'}`;
98
+ exposed[key] = {};
99
+ (bindings[key] ||= []).push({
100
+ HostIp: entry.host_ip || '',
101
+ HostPort: entry.published === undefined ? '' : String(entry.published),
102
+ });
103
+ }
104
+ for (const port of asList(svc.expose))
105
+ exposed[`${port.split('/')[0]}/${port.split('/')[1] || 'tcp'}`] = {};
106
+ return { exposed, bindings };
107
+ }
108
+ /** Volume definitions this node needs, with NFS volumes mounted from the exporter's directory or address. */
109
+ function volumeSpecs(desired, node, used, nodeAddresses) {
110
+ const out = [];
111
+ for (const key of [...used].sort()) {
112
+ const def = desired.compose.volumes?.[key] || {};
113
+ const xe = def['x-tug'] || {};
114
+ const labels = {
115
+ [LABEL_PROJECT]: desired.project,
116
+ 'tug.volume': key,
117
+ ...(def.labels || {}),
118
+ };
119
+ if (def.external) {
120
+ out.push({
121
+ name: String(def.name || key),
122
+ driver: 'local',
123
+ opts: {},
124
+ labels,
125
+ external: true,
126
+ });
127
+ continue;
128
+ }
129
+ const name = volumeName(desired.project, key);
130
+ const nodes = xe.placement?.nodes || [];
131
+ const isNfs = xe.type === 'nfs' || xe.driver === 'nfs' || (xe.type === 'shared' && xe.driver === 'nfs');
132
+ const plain = () => ({
133
+ name,
134
+ driver: String(def.driver || 'local'),
135
+ opts: def.driver_opts || {},
136
+ labels,
137
+ external: false,
138
+ });
139
+ if (isNfs) {
140
+ const exporter = nodes[0];
141
+ const exportPath = xe.export || `/srv/tug/${key}`;
142
+ if (!exporter)
143
+ out.push(plain());
144
+ else if (nodes.includes(node))
145
+ // The exporter mounts the exported directory directly; an NFS server on this node must export it.
146
+ out.push({
147
+ name,
148
+ driver: 'local',
149
+ opts: { type: 'none', o: 'bind', device: exportPath },
150
+ labels,
151
+ external: false,
152
+ });
153
+ else {
154
+ const addr = xe.address || nodeAddresses[exporter] || exporter;
155
+ out.push({
156
+ name,
157
+ driver: 'local',
158
+ opts: { type: 'nfs', o: `addr=${addr},rw,nolock,soft`, device: `:${exportPath}` },
159
+ labels,
160
+ external: false,
161
+ });
162
+ }
163
+ continue;
164
+ }
165
+ if (!nodes.length || nodes.includes(node))
166
+ out.push(plain());
167
+ }
168
+ return out;
169
+ }
170
+ function walkStrings(value, fn) {
171
+ if (typeof value === 'string')
172
+ return fn(value);
173
+ if (Array.isArray(value))
174
+ return value.map((v) => walkStrings(v, fn));
175
+ if (value && typeof value === 'object')
176
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walkStrings(v, fn)]));
177
+ return value;
178
+ }
179
+ export function containerSpecs(desired, node, { secrets = {}, secretVersions = {}, endpoints = {}, nodeAddresses = {}, bind, } = {}) {
180
+ const { project } = desired;
181
+ const network = networkName(project);
182
+ const placed = new Set();
183
+ for (const service of Object.keys(desired.compose.services || {})) {
184
+ const nodes = desired.services[service].placement?.nodes || [];
185
+ if (!nodes.length || nodes.includes(node))
186
+ placed.add(service);
187
+ }
188
+ const hosts = crossNodeHosts(project, endpoints, placed);
189
+ const usedVolumes = new Set();
190
+ const specs = {};
191
+ for (const service of [...placed].sort()) {
192
+ const src = desired.compose.services[service];
193
+ const state = desired.services[service];
194
+ const identity = state.artifact?.imageId ?? state.image;
195
+ const image = state.artifact?.imageRef ?? state.image ?? '';
196
+ const ports = (bind && src.ports
197
+ ? bindPorts(src.ports, bind.address, bind.loopback?.has(service) ? src['x-tug']?.ingress?.port : undefined)
198
+ : src.ports || []);
199
+ const { exposed, bindings } = portMaps(src, ports);
200
+ const binds = [];
201
+ const tmpfs = {};
202
+ for (const mount of src.volumes || []) {
203
+ if (typeof mount === 'string')
204
+ continue;
205
+ const m = mount;
206
+ const ro = m.read_only ? ':ro' : '';
207
+ if (m.type === 'volume') {
208
+ const key = String(m.source);
209
+ usedVolumes.add(key);
210
+ const def = desired.compose.volumes?.[key];
211
+ binds.push(`${def?.external ? (def.name ?? key) : volumeName(project, key)}:${m.target}${ro}`);
212
+ }
213
+ else if (m.type === 'bind')
214
+ binds.push(`${m.source}:${m.target}${ro}`);
215
+ else if (m.type === 'tmpfs') {
216
+ const size = m.tmpfs?.size;
217
+ tmpfs[String(m.target)] = size ? `size=${size}` : '';
218
+ }
219
+ }
220
+ for (const target of asList(src.tmpfs))
221
+ tmpfs[target] = tmpfs[target] ?? '';
222
+ const env = Object.entries(src.environment || {})
223
+ .filter(([, v]) => v !== null && v !== undefined)
224
+ .map(([k, v]) => `${k}=${v}`);
225
+ const memory = sizeBytes(src.mem_limit);
226
+ const reservation = sizeBytes(src.mem_reservation);
227
+ const shm = sizeBytes(src.shm_size);
228
+ const ulimits = Object.entries(src.ulimits || {}).map(([Name, v]) => typeof v === 'number'
229
+ ? { Name, Soft: v, Hard: v }
230
+ : {
231
+ Name,
232
+ Soft: Number(v.soft),
233
+ Hard: Number(v.hard),
234
+ });
235
+ const logging = src.logging;
236
+ const extra = [...extraHosts(src.extra_hosts)];
237
+ const taken = new Set(extra.map((h) => h.split(':')[0]));
238
+ for (const host of hosts)
239
+ if (!taken.has(host.split(':')[0]))
240
+ extra.push(host);
241
+ const stopTimeout = composeDurationMs(src.stop_grace_period);
242
+ const hostConfig = {
243
+ NetworkMode: network,
244
+ ...(binds.length ? { Binds: binds } : {}),
245
+ ...(Object.keys(tmpfs).length ? { Tmpfs: tmpfs } : {}),
246
+ ...(Object.keys(bindings).length ? { PortBindings: bindings } : {}),
247
+ ...(restartPolicy(src.restart) ? { RestartPolicy: restartPolicy(src.restart) } : {}),
248
+ ...(memory !== undefined ? { Memory: memory } : {}),
249
+ ...(reservation !== undefined ? { MemoryReservation: reservation } : {}),
250
+ ...(src.cpus !== undefined ? { NanoCpus: Math.round(Number(src.cpus) * SECOND_NS) } : {}),
251
+ ...(src.pids_limit !== undefined ? { PidsLimit: Number(src.pids_limit) } : {}),
252
+ ...(shm !== undefined ? { ShmSize: shm } : {}),
253
+ ...(src.init !== undefined ? { Init: Boolean(src.init) } : {}),
254
+ ...(src.read_only ? { ReadonlyRootfs: true } : {}),
255
+ ...(extra.length ? { ExtraHosts: extra } : {}),
256
+ ...(asList(src.cap_add).length ? { CapAdd: asList(src.cap_add) } : {}),
257
+ ...(asList(src.cap_drop).length ? { CapDrop: asList(src.cap_drop) } : {}),
258
+ ...(ulimits.length ? { Ulimits: ulimits } : {}),
259
+ ...(logging?.driver
260
+ ? { LogConfig: { Type: logging.driver, Config: logging.options || {} } }
261
+ : {}),
262
+ };
263
+ const hc = healthcheck(src.healthcheck);
264
+ const definition = {
265
+ Image: image,
266
+ ...(env.length ? { Env: env } : {}),
267
+ ...(src.command != null ? { Cmd: asList(src.command) } : {}),
268
+ ...(src.entrypoint != null ? { Entrypoint: asList(src.entrypoint) } : {}),
269
+ ...(src.user ? { User: String(src.user) } : {}),
270
+ ...(src.working_dir ? { WorkingDir: String(src.working_dir) } : {}),
271
+ ...(src.hostname ? { Hostname: String(src.hostname) } : {}),
272
+ Labels: Object.fromEntries(Object.entries(src.labels || {}).map(([k, v]) => [
273
+ k,
274
+ String(v),
275
+ ])),
276
+ ...(Object.keys(exposed).length ? { ExposedPorts: exposed } : {}),
277
+ ...(src.stop_signal ? { StopSignal: String(src.stop_signal) } : {}),
278
+ ...(stopTimeout !== undefined ? { StopTimeout: Math.ceil(stopTimeout / 1000) } : {}),
279
+ ...(hc ? { Healthcheck: hc } : {}),
280
+ HostConfig: hostConfig,
281
+ NetworkingConfig: { EndpointsConfig: { [network]: { Aliases: [service] } } },
282
+ };
283
+ const used = [...new Set(sentinelNames(definition))].sort();
284
+ const versions = Object.fromEntries(used
285
+ .filter((name) => secretVersions[name] != null)
286
+ .map((name) => [name, secretVersions[name]]));
287
+ const hash = crypto
288
+ .createHash('sha256')
289
+ .update(JSON.stringify({ definition: { ...definition, Image: identity }, versions }))
290
+ .digest('hex')
291
+ .slice(0, 16);
292
+ const body = walkStrings(definition, (s) => substituteSentinels(s, secrets));
293
+ body.Labels = {
294
+ ...body.Labels,
295
+ [LABEL_PROJECT]: project,
296
+ [LABEL_SERVICE]: service,
297
+ [LABEL_HASH]: hash,
298
+ ...(desired.revision || desired.version
299
+ ? { [LABEL_REVISION]: desired.revision || desired.version }
300
+ : {}),
301
+ };
302
+ const dependsOn = {};
303
+ const deps = src.depends_on;
304
+ if (Array.isArray(deps)) {
305
+ for (const dep of deps)
306
+ if (placed.has(String(dep)))
307
+ dependsOn[String(dep)] = 'service_started';
308
+ }
309
+ else if (deps && typeof deps === 'object') {
310
+ for (const [dep, spec] of Object.entries(deps))
311
+ if (placed.has(dep))
312
+ dependsOn[dep] =
313
+ spec?.condition === 'service_healthy'
314
+ ? 'service_healthy'
315
+ : 'service_started';
316
+ }
317
+ specs[service] = {
318
+ service,
319
+ name: containerName(project, service, hash),
320
+ hash,
321
+ body,
322
+ dependsOn,
323
+ };
324
+ }
325
+ return { network, volumes: volumeSpecs(desired, node, usedVolumes, nodeAddresses), specs };
326
+ }
327
+ /** Start order: dependencies first, ties by service name; a cycle is an error. */
328
+ export function startOrder(specs) {
329
+ const order = [];
330
+ const state = new Map();
331
+ const visit = (service, path) => {
332
+ if (state.get(service) === 'done')
333
+ return;
334
+ if (state.get(service) === 'visiting')
335
+ throw new Error(`depends_on cycle: ${[...path, service].join(' -> ')}`);
336
+ state.set(service, 'visiting');
337
+ for (const dep of Object.keys(specs[service].dependsOn).sort())
338
+ visit(dep, [...path, service]);
339
+ state.set(service, 'done');
340
+ order.push(service);
341
+ };
342
+ for (const service of Object.keys(specs).sort())
343
+ visit(service, []);
344
+ return order;
345
+ }
346
+ /** `body` with the host side of its (single) published port replaced; used for rolling revisions. */
347
+ export function withHostPort(body, port) {
348
+ const bindings = (body.HostConfig?.PortBindings || {});
349
+ return {
350
+ ...body,
351
+ HostConfig: {
352
+ ...body.HostConfig,
353
+ PortBindings: Object.fromEntries(Object.entries(bindings).map(([key, list]) => [
354
+ key,
355
+ list.map((b) => ({ ...b, HostPort: String(port) })),
356
+ ])),
357
+ },
358
+ };
359
+ }
package/dist/control.js CHANGED
@@ -17,6 +17,7 @@ import { openDb } from "./db.js";
17
17
  import { encrypt, decrypt, loadOrCreateMasterKey } from "./secrets.js";
18
18
  import { buildDesiredState, loopbackIngressServices, serviceSecretNames, validateCompose, } from "./compose.js";
19
19
  import { buildAndPush, unpackContext, run as runCommand } from "./build.js";
20
+ import { sentinelNames } from "./normalize.js";
20
21
  import { ensureDir, errMessage, hashToken, id, now, token } from "./util.js";
21
22
  import { DEFAULT_MESH_CIDR, DEFAULT_WG_PORT, WG_INTERFACE, WG_KEEPALIVE, allocateOverlayIp, isWgKey, parseCidr, wgEndpoint, } from "./mesh.js";
22
23
  import { buildIngressWork, resolveServiceEndpoints } from "./ingress.js";
@@ -1280,6 +1281,14 @@ export async function createApp(options = {}) {
1280
1281
  catch (e) {
1281
1282
  throw httpError(400, errMessage(e));
1282
1283
  }
1284
+ // The CLI interpolates control secrets as sentinels; a reference to a secret that is gone would deploy an empty value.
1285
+ const referenced = sentinelNames(compose);
1286
+ if (referenced.length) {
1287
+ const known = new Set((await db.all('SELECT name FROM secrets WHERE project_id=?', project)).map((row) => row.name));
1288
+ const missing = referenced.filter((name) => !known.has(name));
1289
+ if (missing.length)
1290
+ throw httpError(400, `secrets not found in project ${project}: ${missing.join(', ')}; create them with tug secret-set`);
1291
+ }
1283
1292
  const builtServices = Object.entries(compose.services)
1284
1293
  .filter(([, service]) => service.build)
1285
1294
  .map(([name]) => name);
@@ -0,0 +1,232 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ /** Oldest Engine API the agent speaks (Docker 20.10+, which the nested daemons in the integration tests run). */
4
+ export declare const API_VERSION = "v1.41";
5
+ export declare const DEFAULT_SOCKET = "/var/run/docker.sock";
6
+ /** An Engine API error; `status` is the HTTP status and `message` the daemon's own text. */
7
+ export declare class DockerError extends Error {
8
+ status: number;
9
+ constructor(status: number, message: string);
10
+ }
11
+ export declare const isNotFound: (error: unknown) => boolean;
12
+ export interface ContainerSummary {
13
+ Id: string;
14
+ Names: string[];
15
+ Image: string;
16
+ ImageID?: string;
17
+ State: string;
18
+ Status?: string;
19
+ Created?: number;
20
+ Labels: Record<string, string>;
21
+ Ports?: {
22
+ IP?: string;
23
+ PrivatePort: number;
24
+ PublicPort?: number;
25
+ Type: string;
26
+ }[];
27
+ }
28
+ export interface ContainerInspect {
29
+ Id: string;
30
+ Name: string;
31
+ Image?: string;
32
+ Config?: {
33
+ Image?: string;
34
+ Labels?: Record<string, string>;
35
+ Env?: string[];
36
+ };
37
+ State?: {
38
+ Status?: string;
39
+ Running?: boolean;
40
+ ExitCode?: number;
41
+ Health?: {
42
+ Status?: string;
43
+ };
44
+ };
45
+ NetworkSettings?: {
46
+ Networks?: Record<string, {
47
+ IPAddress?: string;
48
+ GlobalIPv6Address?: string;
49
+ }>;
50
+ };
51
+ [key: string]: unknown;
52
+ }
53
+ export interface ContainerFilter {
54
+ /** `key` or `key=value` label filters, all of which must match. */
55
+ label?: string[];
56
+ /** Containers created from this image reference or ID. */
57
+ ancestor?: string;
58
+ name?: string;
59
+ }
60
+ export interface ImageInspect {
61
+ Id: string;
62
+ Os?: string;
63
+ Architecture?: string;
64
+ RepoTags?: string[];
65
+ RepoDigests?: string[];
66
+ }
67
+ export interface NetworkInspect {
68
+ Name: string;
69
+ Id: string;
70
+ Labels?: Record<string, string>;
71
+ Containers?: Record<string, unknown>;
72
+ }
73
+ export interface VolumeInspect {
74
+ Name: string;
75
+ Driver?: string;
76
+ Labels?: Record<string, string> | null;
77
+ Options?: Record<string, string> | null;
78
+ }
79
+ export interface DaemonInfo {
80
+ OSType?: string;
81
+ Architecture?: string;
82
+ ServerVersion?: string;
83
+ [key: string]: unknown;
84
+ }
85
+ export interface DaemonVersion {
86
+ Version?: string;
87
+ ApiVersion?: string;
88
+ MinAPIVersion?: string;
89
+ [key: string]: unknown;
90
+ }
91
+ /** `POST /containers/create` body; the agent builds these in `containerSpecs`. */
92
+ export interface CreateContainerBody {
93
+ Image: string;
94
+ Env?: string[];
95
+ Cmd?: string[] | null;
96
+ Entrypoint?: string[] | null;
97
+ User?: string;
98
+ WorkingDir?: string;
99
+ Hostname?: string;
100
+ Labels?: Record<string, string>;
101
+ ExposedPorts?: Record<string, object>;
102
+ StopSignal?: string;
103
+ StopTimeout?: number;
104
+ Healthcheck?: Record<string, unknown>;
105
+ HostConfig?: Record<string, unknown>;
106
+ NetworkingConfig?: {
107
+ EndpointsConfig: Record<string, {
108
+ Aliases?: string[];
109
+ [key: string]: unknown;
110
+ }>;
111
+ };
112
+ }
113
+ /** A process attached to a container: exec or logs. Shaped like a child process so `live.ts` treats both alike. */
114
+ export interface AttachedProcess extends EventEmitter {
115
+ stdin: Writable | null;
116
+ stdout: Readable;
117
+ stderr: Readable;
118
+ kill(signal?: NodeJS.Signals): boolean;
119
+ resize?(columns: number, rows: number): void;
120
+ }
121
+ export interface ExecOptions {
122
+ cmd: string[];
123
+ stdin?: boolean;
124
+ tty?: boolean;
125
+ columns?: number;
126
+ rows?: number;
127
+ }
128
+ export interface LogsOptions {
129
+ follow?: boolean;
130
+ tail?: number;
131
+ timestamps?: boolean;
132
+ }
133
+ export interface DockerApi {
134
+ version(): Promise<DaemonVersion>;
135
+ info(): Promise<DaemonInfo>;
136
+ listContainers(filter?: ContainerFilter, all?: boolean): Promise<ContainerSummary[]>;
137
+ inspectContainer(id: string): Promise<ContainerInspect>;
138
+ createContainer(name: string, body: CreateContainerBody): Promise<string>;
139
+ startContainer(id: string): Promise<void>;
140
+ stopContainer(id: string, timeoutSeconds?: number): Promise<void>;
141
+ restartContainer(id: string): Promise<void>;
142
+ /** Removes a container even when running; a missing container is not an error. */
143
+ removeContainer(id: string): Promise<void>;
144
+ /** Connects a running container to a network, optionally under service aliases. */
145
+ connectNetwork(network: string, container: string, aliases?: string[]): Promise<void>;
146
+ /** Container output as demultiplexed, ended streams (a TTY container's output is all stdout). */
147
+ logs(id: string, options?: LogsOptions): Promise<AttachedProcess>;
148
+ /** Copies the tar stream into the container directory `dir`. */
149
+ putArchive(id: string, dir: string, tar: Readable | Buffer): Promise<void>;
150
+ /** Runs a command to completion and returns its output; a non-zero exit is reported in `code`. */
151
+ execRun(id: string, cmd: string[]): Promise<{
152
+ code: number;
153
+ out: string;
154
+ err: string;
155
+ }>;
156
+ /** Starts an interactive or streaming exec; `close` carries the exit code. */
157
+ execStart(id: string, options: ExecOptions): Promise<AttachedProcess>;
158
+ inspectNetwork(name: string): Promise<NetworkInspect | null>;
159
+ createNetwork(name: string, labels: Record<string, string>): Promise<void>;
160
+ inspectVolume(name: string): Promise<VolumeInspect | null>;
161
+ createVolume(name: string, options: {
162
+ driver?: string;
163
+ opts?: Record<string, string>;
164
+ labels?: Record<string, string>;
165
+ }): Promise<void>;
166
+ removeVolume(name: string): Promise<void>;
167
+ inspectImage(ref: string): Promise<ImageInspect | null>;
168
+ /** Pulls `ref`; `auth` becomes `X-Registry-Auth`. */
169
+ pullImage(ref: string, auth?: Record<string, string>): Promise<void>;
170
+ loadImage(archive: Readable | Buffer): Promise<void>;
171
+ removeImage(ref: string): Promise<void>;
172
+ }
173
+ /** Splits a reference into `fromImage` and `tag` for `POST /images/create`. */
174
+ export declare function splitImageRef(ref: string): {
175
+ fromImage: string;
176
+ tag: string;
177
+ };
178
+ /**
179
+ * Splits the 8-byte-header multiplexed stream (`[stream, 0, 0, 0, size(4, big endian)]` frames) that
180
+ * non-TTY attach, exec and logs use into `stdout` and `stderr`. `end` fires once the source ends.
181
+ */
182
+ export declare function demux(source: Readable, stdout: Writable, stderr: Writable): void;
183
+ /** `unix:///path` or a bare path from `DOCKER_HOST`; the agent talks to the local daemon only. */
184
+ export declare function socketFromEnv(env?: NodeJS.ProcessEnv): string;
185
+ export declare class DockerEngine implements DockerApi {
186
+ private readonly socketPath;
187
+ constructor(socketPath?: string);
188
+ private request;
189
+ /** Reads the response; non-2xx becomes a `DockerError` carrying the daemon's message. */
190
+ private call;
191
+ private json;
192
+ /** Consumes the JSON-lines progress stream of pull and load; a line with `error` fails the call. */
193
+ private progress;
194
+ version: () => Promise<DaemonVersion>;
195
+ info: () => Promise<DaemonInfo>;
196
+ listContainers(filter?: ContainerFilter, all?: boolean): Promise<ContainerSummary[]>;
197
+ inspectContainer: (id: string) => Promise<ContainerInspect>;
198
+ createContainer(name: string, body: CreateContainerBody): Promise<string>;
199
+ startContainer(id: string): Promise<void>;
200
+ stopContainer(id: string, timeoutSeconds?: number): Promise<void>;
201
+ restartContainer(id: string): Promise<void>;
202
+ removeContainer(id: string): Promise<void>;
203
+ connectNetwork(network: string, container: string, aliases?: string[]): Promise<void>;
204
+ logs(id: string, options?: LogsOptions): Promise<AttachedProcess>;
205
+ private isTty;
206
+ /**
207
+ * Wraps a response or hijacked socket as a process whose output is demultiplexed unless a TTY.
208
+ * `close` fires with `exitCode()` once the source and both output streams have ended.
209
+ */
210
+ private attachedFromStream;
211
+ putArchive(id: string, dir: string, tar: Readable | Buffer): Promise<void>;
212
+ execRun(id: string, cmd: string[]): Promise<{
213
+ code: number;
214
+ out: string;
215
+ err: string;
216
+ }>;
217
+ execStart(id: string, options: ExecOptions): Promise<AttachedProcess>;
218
+ private resizeExec;
219
+ inspectNetwork(name: string): Promise<NetworkInspect | null>;
220
+ createNetwork(name: string, labels: Record<string, string>): Promise<void>;
221
+ inspectVolume(name: string): Promise<VolumeInspect | null>;
222
+ createVolume(name: string, options: {
223
+ driver?: string;
224
+ opts?: Record<string, string>;
225
+ labels?: Record<string, string>;
226
+ }): Promise<void>;
227
+ removeVolume(name: string): Promise<void>;
228
+ inspectImage(ref: string): Promise<ImageInspect | null>;
229
+ pullImage(ref: string, auth?: Record<string, string>): Promise<void>;
230
+ loadImage(archive: Readable | Buffer): Promise<void>;
231
+ removeImage(ref: string): Promise<void>;
232
+ }