@norskvideo/ctl-test-harness 0.1.74 → 0.1.75
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/container-net.d.ts +3 -1
- package/package.json +1 -1
- package/smoke.d.ts +37 -0
- package/smoke.js +40 -4
package/container-net.d.ts
CHANGED
|
@@ -18,7 +18,9 @@ export declare const authority: (e: Endpoint) => string;
|
|
|
18
18
|
export declare function productServiceHost(instanceId: string, service: string): string;
|
|
19
19
|
export interface ResolveOpts {
|
|
20
20
|
instanceId: string;
|
|
21
|
-
service: "studio"
|
|
21
|
+
/** A compose service name: "studio" and "media" for a Studio product, or a
|
|
22
|
+
* direct-SDK product's own control service (the smoke `surface`). */
|
|
23
|
+
service: string;
|
|
22
24
|
/** Port the listener binds INSIDE the container (studio 8000, media SRT its
|
|
23
25
|
* own configured port). Used in `direct` mode. */
|
|
24
26
|
internalPort: number;
|
package/package.json
CHANGED
package/smoke.d.ts
CHANGED
|
@@ -33,6 +33,25 @@ export interface SmokeContext {
|
|
|
33
33
|
output?: "json" | "yaml";
|
|
34
34
|
}): Promise<CliResult>;
|
|
35
35
|
fetchStudio(path: string): Promise<Response>;
|
|
36
|
+
/** The spec's `surface` (Studio's by default), reached the same two ways:
|
|
37
|
+
* the banded published port, or `<id>-<service>-1:<internalPort>` direct. */
|
|
38
|
+
surfaceUrl(path: string): string;
|
|
39
|
+
/** GET surfaceUrl(path) through the injected reader -- for observations. A
|
|
40
|
+
* prepare step that acts (POST, DELETE) uses fetch(ctx.surfaceUrl(...)). */
|
|
41
|
+
fetchSurface(path: string): Promise<Response>;
|
|
42
|
+
}
|
|
43
|
+
/** The instance service whose HTTP surface the journey publishes and reaches.
|
|
44
|
+
* Studio's by default; a direct-SDK product (no Studio, e.g. norsk-relay)
|
|
45
|
+
* names its own control API. */
|
|
46
|
+
export interface SmokeSurface {
|
|
47
|
+
service: string;
|
|
48
|
+
internalPort: number;
|
|
49
|
+
/** The host port the TEMPLATE publishes the surface on (a `--param` the spec
|
|
50
|
+
* sets). ctl emits `--host-ports` only for Studio and media -- one naming any
|
|
51
|
+
* other service is persisted but never wired -- so a direct-SDK product's
|
|
52
|
+
* surface is published by its own compose. Given, no `--host-ports` is
|
|
53
|
+
* passed and publish mode dials this port. */
|
|
54
|
+
hostPort?: number;
|
|
36
55
|
}
|
|
37
56
|
/** Set on any observation that holds with or without a source — a resilient
|
|
38
57
|
* switch that keeps a ladder fed from a slate, a control surface that is up
|
|
@@ -85,6 +104,14 @@ export interface SmokeSpec {
|
|
|
85
104
|
licenseFile?: string;
|
|
86
105
|
};
|
|
87
106
|
template: SmokeTemplate;
|
|
107
|
+
/** Default { service: "studio", internalPort: 8000 }. Published on the banded
|
|
108
|
+
* studioHostPort; STUDIO_HOST_PORT is passed instead only for Studio's. */
|
|
109
|
+
surface?: SmokeSurface;
|
|
110
|
+
/** Runs once the instance reports running, before the not-yet phase: what a
|
|
111
|
+
* product needs in place before its sources can be accepted (the relay
|
|
112
|
+
* refuses SRT for a stream nobody declared). Not an observation -- it may
|
|
113
|
+
* act, and a throw fails the journey. */
|
|
114
|
+
prepare?(ctx: SmokeContext): Promise<void>;
|
|
88
115
|
/** Extra `--param NAME=value`. INSTANCE_NAME is always passed; STUDIO_HOST_PORT
|
|
89
116
|
* when the template declares it (else the port is pinned with --host-ports). */
|
|
90
117
|
params?: string[];
|
|
@@ -206,5 +233,15 @@ export declare function assertManifestRoutesServed(opts: {
|
|
|
206
233
|
export declare function sourceDependent(o: SmokeObservation): boolean;
|
|
207
234
|
/** One sample of an observation — the predicate the three phases poll. */
|
|
208
235
|
export declare function observationSatisfied(o: SmokeObservation, ctx: SmokeContext, readers: SmokeReaders): Promise<boolean>;
|
|
236
|
+
/** A direct-SDK product's licence identity -- the sdk-client cert and key it
|
|
237
|
+
* answers the engine's V2 handshake with, base64 -- from what smoke.yml
|
|
238
|
+
* exports (repo secrets NORSK_SDK_CLIENT_CERT_B64 / NORSK_SDK_CLIENT_KEY_B64).
|
|
239
|
+
* Undefined when neither is set (a Studio product, or a local run without
|
|
240
|
+
* one); half a pair is a misconfigured repo, so it throws rather than
|
|
241
|
+
* launching an instance that restarts on "License handshake required". */
|
|
242
|
+
export declare function sdkClientIdentityFromEnv(env?: Record<string, string | undefined>): {
|
|
243
|
+
certB64: string;
|
|
244
|
+
keyB64: string;
|
|
245
|
+
} | undefined;
|
|
209
246
|
export declare function runProductSmoke(slug: string, spec: SmokeSpec, deps?: SmokeDeps): Promise<void>;
|
|
210
247
|
export {};
|
package/smoke.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { dumpInstanceContainerLogs, preserveEngineLogs, preserveInstanceContainerLogs } from "./container-logs.js";
|
|
23
|
-
import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
|
|
23
|
+
import { authority, ensureRunnerOnNetwork, netReachMode, resolveEndpoint, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
|
|
24
24
|
import { cleanupDaemon, ctlSupportsEphemeralPorts, PROXY_PORT_FILE, requireLicenseFile, runCli, startDaemon, } from "./daemon.js";
|
|
25
25
|
import { hashSlot } from "./harness-config.js";
|
|
26
26
|
import { ctlSupportsNoPublish, ctlSupportsPublishDebugPorts, runnerContainerUser } from "./launch.js";
|
|
@@ -191,6 +191,22 @@ function parseJson(r, what) {
|
|
|
191
191
|
throw new Error(`${what}: not JSON: ${r.stdout.slice(0, 200)}`);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
/** A direct-SDK product's licence identity -- the sdk-client cert and key it
|
|
195
|
+
* answers the engine's V2 handshake with, base64 -- from what smoke.yml
|
|
196
|
+
* exports (repo secrets NORSK_SDK_CLIENT_CERT_B64 / NORSK_SDK_CLIENT_KEY_B64).
|
|
197
|
+
* Undefined when neither is set (a Studio product, or a local run without
|
|
198
|
+
* one); half a pair is a misconfigured repo, so it throws rather than
|
|
199
|
+
* launching an instance that restarts on "License handshake required". */
|
|
200
|
+
export function sdkClientIdentityFromEnv(env = process.env) {
|
|
201
|
+
const certB64 = env.NORSK_SDK_CLIENT_CERT_B64 ?? "";
|
|
202
|
+
const keyB64 = env.NORSK_SDK_CLIENT_KEY_B64 ?? "";
|
|
203
|
+
if (!certB64 && !keyB64)
|
|
204
|
+
return undefined;
|
|
205
|
+
if (!certB64 || !keyB64) {
|
|
206
|
+
throw new Error(`NORSK_SDK_CLIENT_${certB64 ? "KEY" : "CERT"}_B64 is empty but its pair is set -- set both repo secrets or neither`);
|
|
207
|
+
}
|
|
208
|
+
return { certB64, keyB64 };
|
|
209
|
+
}
|
|
194
210
|
export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
195
211
|
const ports = smokePorts(slug, spec.ports);
|
|
196
212
|
const storeDir = deps.storeDir(slug);
|
|
@@ -202,6 +218,8 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
202
218
|
};
|
|
203
219
|
const mode = netReachMode();
|
|
204
220
|
const studio = { instanceId, studioHostPort: ports.studioHostPort };
|
|
221
|
+
const surface = spec.surface ?? { service: "studio", internalPort: STUDIO_INTERNAL_PORT };
|
|
222
|
+
const studioSurface = surface.service === "studio" && surface.internalPort === STUDIO_INTERNAL_PORT;
|
|
205
223
|
const cli = async (argv, opts = {}) => {
|
|
206
224
|
const full = ["--port", String(ports.daemonPort), ...argv, ...(opts.output ? ["-o", opts.output] : [])];
|
|
207
225
|
return deps.cli(storeDir, full);
|
|
@@ -223,6 +241,18 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
223
241
|
studio,
|
|
224
242
|
cli,
|
|
225
243
|
fetchStudio: (path) => deps.readers.fetch(`${studioBaseFrom(studio, process.env.NORSK_TEST_HOST ?? "localhost", mode)}${path}`),
|
|
244
|
+
surfaceUrl: (path) => {
|
|
245
|
+
const e = resolveEndpoint({
|
|
246
|
+
instanceId,
|
|
247
|
+
service: surface.service,
|
|
248
|
+
internalPort: surface.internalPort,
|
|
249
|
+
publishHost: process.env.NORSK_TEST_HOST ?? "localhost",
|
|
250
|
+
publishPort: surface.hostPort ?? ports.studioHostPort,
|
|
251
|
+
mode,
|
|
252
|
+
});
|
|
253
|
+
return `http://${authority(e)}${path}`;
|
|
254
|
+
},
|
|
255
|
+
fetchSurface: (path) => deps.readers.fetch(ctx.surfaceUrl(path)),
|
|
226
256
|
};
|
|
227
257
|
let daemon = null;
|
|
228
258
|
let launched = false;
|
|
@@ -335,10 +365,12 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
335
365
|
const params = [`INSTANCE_NAME=${instanceId}`, ...(spec.params ?? [])];
|
|
336
366
|
const launchArgv = ["instance", "launch-template", instanceId, "--template", templateName];
|
|
337
367
|
const internalOnly = mode === "direct" && (await deps.supportsNoPublish());
|
|
338
|
-
if (declared.has(STUDIO_HOST_PORT_PARAM))
|
|
368
|
+
if (studioSurface && declared.has(STUDIO_HOST_PORT_PARAM)) {
|
|
339
369
|
params.push(`${STUDIO_HOST_PORT_PARAM}=${ports.studioHostPort}`);
|
|
340
|
-
|
|
341
|
-
|
|
370
|
+
}
|
|
371
|
+
else if (!internalOnly && surface.hostPort === undefined) {
|
|
372
|
+
launchArgv.push("--host-ports", `${ports.studioHostPort}:${surface.internalPort}@${surface.service}`);
|
|
373
|
+
}
|
|
342
374
|
for (const p of params)
|
|
343
375
|
launchArgv.push("--param", p);
|
|
344
376
|
if (spec.hardware)
|
|
@@ -376,6 +408,10 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
376
408
|
const inst = parseJson(r, "instance list").instances?.find((i) => i.id === instanceId);
|
|
377
409
|
return inst?.status === "running" || inst?.status === "healthy";
|
|
378
410
|
}, { timeoutMs: timeouts.healthyMs, intervalMs: 1000, label: `instance ${instanceId} never reported running` });
|
|
411
|
+
if (spec.prepare) {
|
|
412
|
+
await spec.prepare(ctx);
|
|
413
|
+
deps.log(`${slug}: prepare done`);
|
|
414
|
+
}
|
|
379
415
|
// Phases.
|
|
380
416
|
const vacuous = spec.sources.length === 0;
|
|
381
417
|
if (vacuous) {
|