@norskvideo/ctl-test-harness 0.1.75 → 0.1.76
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/compose-advancing.js +2 -1
- package/container-net.d.ts +43 -1
- package/container-net.js +131 -10
- package/daemon.d.ts +4 -0
- package/daemon.js +5 -0
- package/demo/harness.js +1 -0
- package/demo/run.d.ts +6 -0
- package/demo/run.js +19 -1
- package/demo/spec.d.ts +2 -1
- package/package.json +1 -1
- package/studio-state.js +9 -6
package/compose-advancing.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// -> nodes[].mediaIn). Playout's scripts/ensure-sources.sh:52-64 as TypeScript
|
|
7
7
|
// (05-demo s7 step 2), pure over an injected fetch so it is unit-tested and
|
|
8
8
|
// reachable through whichever base the caller has: the daemon proxy's
|
|
9
|
-
// `/instance/<id>/visualiser`, or `<id>-media-1:6791/visualiser`
|
|
9
|
+
// `/instance/<id>/visualiser`, or `<id>-media-1:6791/visualiser` from the
|
|
10
|
+
// instance network `<id>_default` (media is on no other).
|
|
10
11
|
export const DEFAULT_COMPOSE_NODE = "video_compose";
|
|
11
12
|
const DEFAULT_SETTLE_MS = 6000;
|
|
12
13
|
/** The highest mediaIn across every node of that name; 0 when none. */
|
package/container-net.d.ts
CHANGED
|
@@ -118,11 +118,13 @@ export declare function ownContainerId(opts?: {
|
|
|
118
118
|
isDockerObject?: (id: string) => boolean;
|
|
119
119
|
}): string | null;
|
|
120
120
|
/** `docker network connect` is not idempotent — a second attach errors. Treat
|
|
121
|
-
* the "already attached" variants as success so setup can call this blindly.
|
|
121
|
+
* the "already attached" variants as success so setup can call this blindly.
|
|
122
|
+
* Twin of norsk-ctl backend/src/docker/container-ops.ts isAlreadyOnNetwork. */
|
|
122
123
|
export declare function isAlreadyOnNetworkError(stderr: string): boolean;
|
|
123
124
|
type RunResult = {
|
|
124
125
|
status: number | null;
|
|
125
126
|
stderr: string;
|
|
127
|
+
stdout?: string;
|
|
126
128
|
};
|
|
127
129
|
type Runner = (cmd: string, args: string[]) => RunResult;
|
|
128
130
|
/** Idempotently join the test container to norsk-net so `direct` service DNS
|
|
@@ -139,4 +141,44 @@ export declare function ensureRunnerOnNetwork(opts?: {
|
|
|
139
141
|
detect?: Parameters<typeof ownContainerId>[0];
|
|
140
142
|
run?: Runner;
|
|
141
143
|
}): "attached" | "already" | "not-in-container" | "failed";
|
|
144
|
+
/** The compose project is the instance id, so its network is `<id>_default`.
|
|
145
|
+
* Mirrors norsk-ctl's instanceNetworkName. */
|
|
146
|
+
export declare function instanceNetworkName(instanceId: string): string;
|
|
147
|
+
/** docker's wording for a disconnect that has nothing to do. Twin of norsk-ctl
|
|
148
|
+
* backend/src/docker/container-ops.ts isNotOnNetwork: change both together. */
|
|
149
|
+
export declare function isNotOnNetworkError(stderr: string): boolean;
|
|
150
|
+
export declare class InstanceNetworks {
|
|
151
|
+
private readonly opts;
|
|
152
|
+
private readonly ids;
|
|
153
|
+
private resolvedId;
|
|
154
|
+
constructor(opts?: {
|
|
155
|
+
/** Explicit id (tests). Omitted -> resolved once by `ownContainerId`. */
|
|
156
|
+
containerId?: string | null;
|
|
157
|
+
run?: Runner;
|
|
158
|
+
});
|
|
159
|
+
private containerId;
|
|
160
|
+
/** Join the runner to the instance's network. Connects on every call rather
|
|
161
|
+
* than trusting the record: an instance deleted and relaunched under the
|
|
162
|
+
* same id has a new network the runner is not on. One `docker network
|
|
163
|
+
* connect` costs the same as the inspect that checking the record would. */
|
|
164
|
+
join(instanceId: string): "attached" | "already" | "not-in-container" | "failed";
|
|
165
|
+
/** Leave a network joined here. Never throws: teardown carries on regardless.
|
|
166
|
+
* `force` leaves even if this process did not record the join (a later
|
|
167
|
+
* `demo down` in a fresh process). */
|
|
168
|
+
leave(instanceId: string, opts?: {
|
|
169
|
+
force?: boolean;
|
|
170
|
+
}): void;
|
|
171
|
+
/** The runner's IP on the instance network, for something media dials back
|
|
172
|
+
* (a capture server). Joins first. Null off-container or on failure. */
|
|
173
|
+
addressOn(instanceId: string): string | null;
|
|
174
|
+
leaveAll(): void;
|
|
175
|
+
joined(): string[];
|
|
176
|
+
}
|
|
177
|
+
/** The process-wide registry the address helpers join through. */
|
|
178
|
+
export declare function currentInstanceNetworks(): InstanceNetworks;
|
|
179
|
+
/** Swap the registry (tests); returns a restore function. */
|
|
180
|
+
export declare function useInstanceNetworks(next: InstanceNetworks): () => void;
|
|
181
|
+
/** Make `<id>-media-1` resolvable in direct mode. Publish mode reaches media by
|
|
182
|
+
* published port and needs nothing. */
|
|
183
|
+
export declare function reachInstanceMedia(instanceId: string, mode?: NetReachMode): void;
|
|
142
184
|
export {};
|
package/container-net.js
CHANGED
|
@@ -23,12 +23,14 @@
|
|
|
23
23
|
// makes for media HTTP — generalised to the studio-control + SRT-egress ports
|
|
24
24
|
// that are the actual colliders.
|
|
25
25
|
//
|
|
26
|
-
// THE ONE CONSTRAINT: container-DNS reachability needs the test container
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
26
|
+
// THE ONE CONSTRAINT: container-DNS reachability needs the test container on
|
|
27
|
+
// the target's network (bridge DNS is per-network): norsk-net for studio, the
|
|
28
|
+
// instance network `<id>_default` for media (see "Instance networks" below).
|
|
29
|
+
// On Docker Desktop / OrbStack the bridge is hidden inside a VM and is not
|
|
30
|
+
// routable from the host at all, so the direct path can't work there — those
|
|
31
|
+
// runs MUST keep publishing. Hence the mode switch: `direct` on the Linux DooD
|
|
32
|
+
// CI box, `publish` (the default) everywhere else. Nothing here changes
|
|
33
|
+
// behaviour until a caller opts a suite into `direct`.
|
|
32
34
|
import { spawnSync } from "node:child_process";
|
|
33
35
|
import { existsSync, readFileSync } from "node:fs";
|
|
34
36
|
import { hostname } from "node:os";
|
|
@@ -83,6 +85,7 @@ export function studioApiBase(o) {
|
|
|
83
85
|
* `<id>-media-1:8080`. Mirrors studio-state.mediaDirectUrl, generalised for the
|
|
84
86
|
* direct-pull capture path. */
|
|
85
87
|
export function mediaHttpBase(o) {
|
|
88
|
+
reachInstanceMedia(o.instanceId, o.mode);
|
|
86
89
|
const e = resolveEndpoint({
|
|
87
90
|
instanceId: o.instanceId,
|
|
88
91
|
service: "media",
|
|
@@ -100,6 +103,7 @@ export function mediaHttpBase(o) {
|
|
|
100
103
|
* and `publishPort` are the same band value today — going direct is what later
|
|
101
104
|
* lets that band be dropped entirely. */
|
|
102
105
|
export function srtEgressUrl(o) {
|
|
106
|
+
reachInstanceMedia(o.instanceId, o.mode);
|
|
103
107
|
const e = resolveEndpoint({
|
|
104
108
|
instanceId: o.instanceId,
|
|
105
109
|
service: "media",
|
|
@@ -127,8 +131,8 @@ export function studioBaseFrom(studio, publishHost, mode) {
|
|
|
127
131
|
});
|
|
128
132
|
}
|
|
129
133
|
// ── Runner network attach ───────────────────────────────────────────────────
|
|
130
|
-
// `direct` DNS only resolves if the test container is on norsk-net.
|
|
131
|
-
// helpers attach it idempotently; all pure/injectable so the logic is tested
|
|
134
|
+
// `direct` DNS for studio only resolves if the test container is on norsk-net.
|
|
135
|
+
// These helpers attach it idempotently; all pure/injectable so the logic is tested
|
|
132
136
|
// without touching docker.
|
|
133
137
|
/** The 64-hex container id from a /proc/self/cgroup dump, or null on a bare host.
|
|
134
138
|
* In a container the id also appears as the hostname, but cgroup is present in
|
|
@@ -180,13 +184,14 @@ export function ownContainerId(opts = {}) {
|
|
|
180
184
|
});
|
|
181
185
|
}
|
|
182
186
|
/** `docker network connect` is not idempotent — a second attach errors. Treat
|
|
183
|
-
* the "already attached" variants as success so setup can call this blindly.
|
|
187
|
+
* the "already attached" variants as success so setup can call this blindly.
|
|
188
|
+
* Twin of norsk-ctl backend/src/docker/container-ops.ts isAlreadyOnNetwork. */
|
|
184
189
|
export function isAlreadyOnNetworkError(stderr) {
|
|
185
190
|
return /already exists in network|is already connected|endpoint with name .* already exists/i.test(stderr);
|
|
186
191
|
}
|
|
187
192
|
const defaultRunner = (cmd, args) => {
|
|
188
193
|
const r = spawnSync(cmd, args, { encoding: "utf-8" });
|
|
189
|
-
return { status: r.status, stderr: r.stderr ?? "" };
|
|
194
|
+
return { status: r.status, stderr: r.stderr ?? "", stdout: r.stdout ?? "" };
|
|
190
195
|
};
|
|
191
196
|
/** Idempotently join the test container to norsk-net so `direct` service DNS
|
|
192
197
|
* resolves. Returns a verdict rather than throwing: `not-in-container` (nothing
|
|
@@ -205,3 +210,119 @@ export function ensureRunnerOnNetwork(opts = {}) {
|
|
|
205
210
|
return "already";
|
|
206
211
|
return "failed";
|
|
207
212
|
}
|
|
213
|
+
// ── Instance networks ───────────────────────────────────────────────────────
|
|
214
|
+
// ctl attaches media to its instance's own compose network only, not to
|
|
215
|
+
// norsk-net: Docker Desktop's port forwarder delivers published UDP to either IP
|
|
216
|
+
// of a dual-homed container, and an SRT listener's reply then leaves by the
|
|
217
|
+
// other interface and is dropped (norsk-ctl docs/product-template-format.md,
|
|
218
|
+
// Networking). So `<id>-media-1` resolves only from `<id>_default`, and direct
|
|
219
|
+
// mode joins the runner to that network whenever it addresses an instance's
|
|
220
|
+
// media.
|
|
221
|
+
//
|
|
222
|
+
// The runner must leave again BEFORE the instance is deleted: a foreign
|
|
223
|
+
// endpoint keeps `compose down` from removing the network, which then leaks.
|
|
224
|
+
// cleanupDaemon does that for every network joined here.
|
|
225
|
+
/** The compose project is the instance id, so its network is `<id>_default`.
|
|
226
|
+
* Mirrors norsk-ctl's instanceNetworkName. */
|
|
227
|
+
export function instanceNetworkName(instanceId) {
|
|
228
|
+
return `${instanceId}_default`;
|
|
229
|
+
}
|
|
230
|
+
/** docker's wording for a disconnect that has nothing to do. Twin of norsk-ctl
|
|
231
|
+
* backend/src/docker/container-ops.ts isNotOnNetwork: change both together. */
|
|
232
|
+
export function isNotOnNetworkError(stderr) {
|
|
233
|
+
return /is not connected to (the )?network|network .* not found|no such network/i.test(stderr);
|
|
234
|
+
}
|
|
235
|
+
export class InstanceNetworks {
|
|
236
|
+
opts;
|
|
237
|
+
ids = new Set();
|
|
238
|
+
resolvedId;
|
|
239
|
+
constructor(opts = {}) {
|
|
240
|
+
this.opts = opts;
|
|
241
|
+
}
|
|
242
|
+
containerId() {
|
|
243
|
+
if (this.opts.containerId !== undefined)
|
|
244
|
+
return this.opts.containerId;
|
|
245
|
+
if (this.resolvedId === undefined)
|
|
246
|
+
this.resolvedId = ownContainerId();
|
|
247
|
+
return this.resolvedId;
|
|
248
|
+
}
|
|
249
|
+
/** Join the runner to the instance's network. Connects on every call rather
|
|
250
|
+
* than trusting the record: an instance deleted and relaunched under the
|
|
251
|
+
* same id has a new network the runner is not on. One `docker network
|
|
252
|
+
* connect` costs the same as the inspect that checking the record would. */
|
|
253
|
+
join(instanceId) {
|
|
254
|
+
const verdict = ensureRunnerOnNetwork({
|
|
255
|
+
network: instanceNetworkName(instanceId),
|
|
256
|
+
containerId: this.containerId(),
|
|
257
|
+
...(this.opts.run ? { run: this.opts.run } : {}),
|
|
258
|
+
});
|
|
259
|
+
if (verdict === "attached" || verdict === "already")
|
|
260
|
+
this.ids.add(instanceId);
|
|
261
|
+
return verdict;
|
|
262
|
+
}
|
|
263
|
+
/** Leave a network joined here. Never throws: teardown carries on regardless.
|
|
264
|
+
* `force` leaves even if this process did not record the join (a later
|
|
265
|
+
* `demo down` in a fresh process). */
|
|
266
|
+
leave(instanceId, opts = {}) {
|
|
267
|
+
if (!this.ids.delete(instanceId) && !opts.force)
|
|
268
|
+
return;
|
|
269
|
+
const id = this.containerId();
|
|
270
|
+
if (!id)
|
|
271
|
+
return;
|
|
272
|
+
const r = (this.opts.run ?? defaultRunner)("docker", [
|
|
273
|
+
"network",
|
|
274
|
+
"disconnect",
|
|
275
|
+
instanceNetworkName(instanceId),
|
|
276
|
+
id,
|
|
277
|
+
]);
|
|
278
|
+
if (r.status !== 0 && !isNotOnNetworkError(r.stderr))
|
|
279
|
+
console.error(`leaving ${instanceNetworkName(instanceId)} failed: ${r.stderr.trim()}`);
|
|
280
|
+
}
|
|
281
|
+
/** The runner's IP on the instance network, for something media dials back
|
|
282
|
+
* (a capture server). Joins first. Null off-container or on failure. */
|
|
283
|
+
addressOn(instanceId) {
|
|
284
|
+
const verdict = this.join(instanceId);
|
|
285
|
+
if (verdict === "not-in-container" || verdict === "failed")
|
|
286
|
+
return null;
|
|
287
|
+
const id = this.containerId();
|
|
288
|
+
if (!id)
|
|
289
|
+
return null;
|
|
290
|
+
const net = instanceNetworkName(instanceId);
|
|
291
|
+
const r = (this.opts.run ?? defaultRunner)("docker", [
|
|
292
|
+
"inspect",
|
|
293
|
+
"-f",
|
|
294
|
+
`{{with index .NetworkSettings.Networks "${net}"}}{{.IPAddress}}{{end}}`,
|
|
295
|
+
id,
|
|
296
|
+
]);
|
|
297
|
+
const ip = (r.stdout ?? "").trim();
|
|
298
|
+
return r.status === 0 && ip ? ip : null;
|
|
299
|
+
}
|
|
300
|
+
leaveAll() {
|
|
301
|
+
for (const id of [...this.ids])
|
|
302
|
+
this.leave(id);
|
|
303
|
+
}
|
|
304
|
+
joined() {
|
|
305
|
+
return [...this.ids];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
let instanceNetworks = new InstanceNetworks();
|
|
309
|
+
/** The process-wide registry the address helpers join through. */
|
|
310
|
+
export function currentInstanceNetworks() {
|
|
311
|
+
return instanceNetworks;
|
|
312
|
+
}
|
|
313
|
+
/** Swap the registry (tests); returns a restore function. */
|
|
314
|
+
export function useInstanceNetworks(next) {
|
|
315
|
+
const prev = instanceNetworks;
|
|
316
|
+
instanceNetworks = next;
|
|
317
|
+
return () => {
|
|
318
|
+
instanceNetworks = prev;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/** Make `<id>-media-1` resolvable in direct mode. Publish mode reaches media by
|
|
322
|
+
* published port and needs nothing. */
|
|
323
|
+
export function reachInstanceMedia(instanceId, mode = netReachMode()) {
|
|
324
|
+
if (mode !== "direct")
|
|
325
|
+
return;
|
|
326
|
+
if (instanceNetworks.join(instanceId) === "failed")
|
|
327
|
+
console.error(`could not join ${instanceNetworkName(instanceId)}: ${instanceId}-media-1 will not resolve`);
|
|
328
|
+
}
|
package/daemon.d.ts
CHANGED
|
@@ -140,6 +140,10 @@ export declare function cleanupDaemon(opts: {
|
|
|
140
140
|
containers: string[];
|
|
141
141
|
/** Injected for tests; defaults to the Docker-label reap. */
|
|
142
142
|
reapInstance?: (id: string) => string[];
|
|
143
|
+
/** Injected for tests; defaults to leaving the instance network the harness
|
|
144
|
+
* joined (container-net InstanceNetworks). It must precede the delete: a
|
|
145
|
+
* foreign endpoint keeps `compose down` from removing the network. */
|
|
146
|
+
leaveNetwork?: (id: string) => void;
|
|
143
147
|
deleteTimeoutMs?: number;
|
|
144
148
|
}): Promise<void>;
|
|
145
149
|
/** Tear down a direct-API test: stop proxy, remove instances, rm temp dir. */
|
package/daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { createServer } from "node:net";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { cliCommand } from "./cli-command.js";
|
|
16
|
+
import { currentInstanceNetworks } from "./container-net.js";
|
|
16
17
|
import { rememberDaemonStore } from "./daemon-stores.js";
|
|
17
18
|
import { pollUntil } from "./poll.js";
|
|
18
19
|
import { reapInstance } from "./reap.js";
|
|
@@ -339,7 +340,9 @@ export const INSTANCE_DELETE_TIMEOUT_MS = 20_000;
|
|
|
339
340
|
*/
|
|
340
341
|
export async function cleanupDaemon(opts) {
|
|
341
342
|
const deleteTimeoutMs = opts.deleteTimeoutMs ?? INSTANCE_DELETE_TIMEOUT_MS;
|
|
343
|
+
const leave = opts.leaveNetwork ?? ((id) => currentInstanceNetworks().leave(id));
|
|
342
344
|
for (const id of opts.instances ?? []) {
|
|
345
|
+
leave(id);
|
|
343
346
|
try {
|
|
344
347
|
if ((await settleWithin(opts.deleteInstance(id), deleteTimeoutMs)) === "timeout")
|
|
345
348
|
console.error(`cleanup: delete ${id} gave no answer in ${deleteTimeoutMs}ms`);
|
|
@@ -348,6 +351,8 @@ export async function cleanupDaemon(opts) {
|
|
|
348
351
|
console.error(`cleanup: delete ${id} failed: ${e}`);
|
|
349
352
|
}
|
|
350
353
|
}
|
|
354
|
+
if (!opts.leaveNetwork)
|
|
355
|
+
currentInstanceNetworks().leaveAll();
|
|
351
356
|
const reap = opts.reapInstance ?? reapInstance;
|
|
352
357
|
for (const id of opts.instances ?? []) {
|
|
353
358
|
const left = reap(id);
|
package/demo/harness.js
CHANGED
|
@@ -179,6 +179,7 @@ export class ProductHarness {
|
|
|
179
179
|
attempt: () => s.cliOk(launchArgv),
|
|
180
180
|
log: deps.log,
|
|
181
181
|
});
|
|
182
|
+
s.joinExtras(instanceId);
|
|
182
183
|
await s.waitRunning(instanceId);
|
|
183
184
|
const rows = await s.ingestPorts(instanceId);
|
|
184
185
|
const templateParams = { declared, overrides: specParams };
|
package/demo/run.d.ts
CHANGED
|
@@ -356,6 +356,12 @@ export declare class DemoSession {
|
|
|
356
356
|
/** name -> stringified default, from `template show`. */
|
|
357
357
|
declaredParams(templateName: string): Promise<Map<string, string | undefined>>;
|
|
358
358
|
startExtras(instanceId: string): void;
|
|
359
|
+
/** ctl puts media on its instance network only, so an extra media dials (a
|
|
360
|
+
* capture origin) must join that network too. It exists only once the
|
|
361
|
+
* launch has created the instance, so this runs after the launch, not with
|
|
362
|
+
* startExtras. Extras are removed before the instance is deleted, so this
|
|
363
|
+
* never holds the network open. */
|
|
364
|
+
joinExtras(instanceId: string): void;
|
|
359
365
|
stopExtras(): void;
|
|
360
366
|
waitRunning(instanceId: string): Promise<void>;
|
|
361
367
|
instanceListed(instanceId: string): Promise<boolean>;
|
package/demo/run.js
CHANGED
|
@@ -30,7 +30,7 @@ import { homedir } from "node:os";
|
|
|
30
30
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
31
31
|
import { parseManifestSeed, repoOf } from "@norskvideo/ctl-sdk/manifest-seed";
|
|
32
32
|
import { preserveInstanceContainerLogs } from "../container-logs.js";
|
|
33
|
-
import { DOCKER_NETWORK_NAME, ensureRunnerOnNetwork, netReachMode } from "../container-net.js";
|
|
33
|
+
import { currentInstanceNetworks, DOCKER_NETWORK_NAME, ensureRunnerOnNetwork, instanceNetworkName, isAlreadyOnNetworkError, netReachMode, } from "../container-net.js";
|
|
34
34
|
import { cleanupDaemon, ctlSupportsEphemeralPorts, PROXY_PORT_FILE, requireLicenseFile, runCli, startDaemon, } from "../daemon.js";
|
|
35
35
|
import { hashSlot } from "../harness-config.js";
|
|
36
36
|
import { ctlSupportsNoPublish, ctlSupportsPublishDebugPorts, runnerContainerUser } from "../launch.js";
|
|
@@ -565,6 +565,7 @@ export class DemoSession {
|
|
|
565
565
|
* the product publishes (plus the spec's built one), the product and its
|
|
566
566
|
* control-plane container all go first. Each step tolerates absence. */
|
|
567
567
|
async resetRegistration(instanceId) {
|
|
568
|
+
currentInstanceNetworks().leave(instanceId, { force: true });
|
|
568
569
|
await this.cli(["instance", "delete", instanceId, "--purge"]);
|
|
569
570
|
const listed = await this.cli(["template", "list"], { output: "json" });
|
|
570
571
|
const names = listed.exitCode === 0
|
|
@@ -695,6 +696,19 @@ export class DemoSession {
|
|
|
695
696
|
this.deps.log(`extra ${e.name}: ${e.image}`);
|
|
696
697
|
}
|
|
697
698
|
}
|
|
699
|
+
/** ctl puts media on its instance network only, so an extra media dials (a
|
|
700
|
+
* capture origin) must join that network too. It exists only once the
|
|
701
|
+
* launch has created the instance, so this runs after the launch, not with
|
|
702
|
+
* startExtras. Extras are removed before the instance is deleted, so this
|
|
703
|
+
* never holds the network open. */
|
|
704
|
+
joinExtras(instanceId) {
|
|
705
|
+
for (const name of this.extras) {
|
|
706
|
+
const c = this.deps.docker(["network", "connect", instanceNetworkName(instanceId), name]);
|
|
707
|
+
if (c.exitCode !== 0 && !isAlreadyOnNetworkError(c.stderr)) {
|
|
708
|
+
throw new Error(`extra '${name}' could not join ${instanceNetworkName(instanceId)}: ${c.stderr.trim()}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
698
712
|
stopExtras() {
|
|
699
713
|
for (const name of this.extras)
|
|
700
714
|
this.deps.docker(["rm", "-f", name]);
|
|
@@ -748,8 +762,11 @@ export class DemoSession {
|
|
|
748
762
|
this.preserveContainerLogs(opts.instances);
|
|
749
763
|
if (opts.handles.length)
|
|
750
764
|
await this.deps.stopSources(opts.handles).catch(() => { });
|
|
765
|
+
// Extras sit on the instance networks; they go before any instance does.
|
|
766
|
+
this.stopExtras();
|
|
751
767
|
if (this.policy === "reuse") {
|
|
752
768
|
for (const id of opts.instances) {
|
|
769
|
+
currentInstanceNetworks().leave(id);
|
|
753
770
|
const r = await this.cli(["instance", "delete", id, "--purge"]);
|
|
754
771
|
if (r.exitCode !== 0)
|
|
755
772
|
this.deps.log(`instance delete ${id}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -836,6 +853,7 @@ export async function demoDown(product, deps = defaultDemoDeps(process.cwd())) {
|
|
|
836
853
|
const cli = (argv) => deps.cli(state.storeDir, ["--port", String(state.daemonPort), ...argv]);
|
|
837
854
|
if (state.daemon === "reuse") {
|
|
838
855
|
if (state.instanceId) {
|
|
856
|
+
currentInstanceNetworks().leave(state.instanceId, { force: true });
|
|
839
857
|
const r = await cli(["instance", "delete", state.instanceId, "--purge"]);
|
|
840
858
|
if (r.exitCode !== 0)
|
|
841
859
|
deps.log(`instance delete ${state.instanceId}: ${(r.stderr || r.stdout).trim()}`);
|
package/demo/spec.d.ts
CHANGED
|
@@ -93,7 +93,8 @@ export type DemoTemplate = {
|
|
|
93
93
|
};
|
|
94
94
|
/** A sidecar container the demo needs beside the instance (funke's ad-push
|
|
95
95
|
* capture origin): `docker run -d --name <name> --network norsk-net <args>
|
|
96
|
-
* <image> <command>`, started before the launch,
|
|
96
|
+
* <image> <command>`, started before the launch, joined to the instance
|
|
97
|
+
* network after it (media is on no other), removed at teardown. */
|
|
97
98
|
export interface DemoExtra {
|
|
98
99
|
name: string;
|
|
99
100
|
image: string;
|
package/package.json
CHANGED
package/studio-state.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// Studio's /live/api/components returns each entry as `{ componentId, componentType, capabilities }`
|
|
19
19
|
// — NOT `{ id, type }` (which is what the source-yaml schema uses internally).
|
|
20
20
|
// Verified empirically against a live Studio instance.
|
|
21
|
-
import { studioBaseFrom } from "./container-net.js";
|
|
21
|
+
import { reachInstanceMedia, studioBaseFrom } from "./container-net.js";
|
|
22
22
|
// The host the studio port is reachable on. Normally the product template
|
|
23
23
|
// publishes it on the loopback of the machine running the tests, so localhost is
|
|
24
24
|
// right. When the test process itself runs inside a container that launches the
|
|
@@ -55,11 +55,13 @@ export function applyTestHost(rawUrl) {
|
|
|
55
55
|
// proxies `/instance/<id>/media/*` here; whip-driver hits the same port direct.
|
|
56
56
|
const MEDIA_HTTP_PORT = 8080;
|
|
57
57
|
// Rewrite a daemon-proxy-routed media URL to a direct hit on the Norsk media
|
|
58
|
-
// container over
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// directly by its compose service name:
|
|
58
|
+
// container over its instance network `<id>_default`, the only one media is
|
|
59
|
+
// on. Studio advertises playback URLs as
|
|
60
|
+
// `http://<publicHost>/instance/<id>/media/<native>` — that route is served ONLY
|
|
61
|
+
// by the daemon's nginx proxy. A harness that runs proxy-less can instead reach
|
|
62
|
+
// the media container directly by its compose service name:
|
|
63
|
+
// `http://<id>-media-1:8080/<native>`; in direct mode this joins the runner to
|
|
64
|
+
// `<id>_default` so that name resolves.
|
|
63
65
|
// The `/instance/<id>/media` prefix is the proxy's routing concern; Norsk's
|
|
64
66
|
// native path is what follows it. Falls back to applyTestHost for URLs that
|
|
65
67
|
// don't carry the proxy prefix. Mirrors whip-driver's dockerNetUrl.
|
|
@@ -70,6 +72,7 @@ export function mediaDirectUrl(rawUrl) {
|
|
|
70
72
|
if (m) {
|
|
71
73
|
const id = m[1];
|
|
72
74
|
const rest = m[2] ?? "/";
|
|
75
|
+
reachInstanceMedia(id);
|
|
73
76
|
return `http://${id}-media-1:${MEDIA_HTTP_PORT}${rest}${u.search}`;
|
|
74
77
|
}
|
|
75
78
|
}
|