@wpelevator/tug 0.2.1 → 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.
- package/README.md +38 -4
- package/dist/agent.d.ts +66 -6
- package/dist/agent.js +851 -170
- package/dist/archive.js +9 -3
- package/dist/artifacts.js +97 -34
- package/dist/build.d.ts +1 -1
- package/dist/build.js +17 -8
- package/dist/cli-build.js +34 -8
- package/dist/cli-config.js +16 -4
- package/dist/cli-install.d.ts +10 -0
- package/dist/cli-install.js +38 -4
- package/dist/cli-live-transport.d.ts +3 -0
- package/dist/cli-live-transport.js +230 -0
- package/dist/cli-live.d.ts +77 -0
- package/dist/cli-live.js +249 -0
- package/dist/cli.js +408 -90
- package/dist/compose.d.ts +35 -9
- package/dist/compose.js +240 -132
- package/dist/container-spec.d.ts +56 -0
- package/dist/container-spec.js +359 -0
- package/dist/control.d.ts +5 -3
- package/dist/control.js +1047 -146
- package/dist/db.d.ts +1 -0
- package/dist/db.js +7 -1
- package/dist/docker-api.d.ts +232 -0
- package/dist/docker-api.js +416 -0
- package/dist/ingress.d.ts +16 -8
- package/dist/ingress.js +84 -29
- package/dist/live.d.ts +83 -0
- package/dist/live.js +276 -0
- package/dist/main.js +3 -1
- package/dist/mesh.js +32 -13
- package/dist/normalize.d.ts +20 -0
- package/dist/normalize.js +75 -0
- package/dist/protocol.d.ts +158 -0
- package/dist/protocol.js +16 -2
- package/dist/reconciler.d.ts +89 -0
- package/dist/reconciler.js +362 -0
- package/dist/runners.d.ts +1 -6
- package/dist/runners.js +1 -5
- package/dist/secrets.js +3 -1
- package/dist/templates/control-compose.yml +1 -0
- package/dist/templates/install-docker.sh +12 -7
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +35 -9
- package/dist/util.d.ts +7 -0
- package/dist/util.js +73 -16
- package/package.json +8 -4
- package/dist/rollout.d.ts +0 -72
- package/dist/rollout.js +0 -319
|
@@ -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.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { saveImageArchive, inspectImage, validateImageArchive } from './artifacts.ts';
|
|
4
4
|
import https from 'node:https';
|
|
5
|
+
import http from 'node:http';
|
|
5
6
|
import { openDb } from './db.ts';
|
|
6
7
|
import type { NodeRow } from './db.ts';
|
|
7
8
|
import { buildAndPush, run as runCommand } from './build.ts';
|
|
@@ -19,7 +20,8 @@ export interface ControlImageOperations {
|
|
|
19
20
|
}
|
|
20
21
|
export declare function createApp(options?: {
|
|
21
22
|
images?: Partial<ControlImageOperations>;
|
|
22
|
-
openDatabase?: typeof openDb;
|
|
23
|
-
eventPruneIntervalMs?: number;
|
|
23
|
+
openDatabase?: typeof openDb;
|
|
24
|
+
/** Minimum time between event prunes triggered by agent reports; default one hour. */ eventPruneIntervalMs?: number;
|
|
25
|
+
/** Override the live open/agent startup timeout in milliseconds. */ liveStartTimeoutMs?: number;
|
|
24
26
|
}): Promise<import("express-serve-static-core").Express>;
|
|
25
|
-
export declare function listen(app: express.Express, port?: number): Promise<
|
|
27
|
+
export declare function listen(app: express.Express, port?: number): Promise<http.Server | https.Server>;
|