@norskvideo/ctl-test-harness 0.1.35 → 0.1.36
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/demo/run.js +2 -2
- package/package.json +1 -1
- package/root-nuke.d.ts +15 -7
- package/root-nuke.js +28 -11
- package/smoke.js +2 -2
- package/temp-dir.js +1 -1
package/demo/run.js
CHANGED
|
@@ -36,7 +36,7 @@ import { ctlSupportsNoPublish, runnerContainerUser } from "../launch.js";
|
|
|
36
36
|
import { pollUntil } from "../poll.js";
|
|
37
37
|
import { nukePathAsRoot, reportForeignOwners } from "../root-nuke.js";
|
|
38
38
|
import { startSrtSources } from "../source-pump.js";
|
|
39
|
-
import { makeStoreDir } from "../temp-dir.js";
|
|
39
|
+
import { makeStoreDir, TEST_TMP_BASE } from "../temp-dir.js";
|
|
40
40
|
const DEMO_PORT_BASE = 35000;
|
|
41
41
|
const DEMO_BAND_WIDTH = 20;
|
|
42
42
|
const DEMO_BANDS = 50;
|
|
@@ -290,7 +290,7 @@ export function defaultDemoDeps(cwd) {
|
|
|
290
290
|
await h.stop();
|
|
291
291
|
},
|
|
292
292
|
cleanup: (opts) => cleanupDaemon({ ...opts, stopProxy: async () => { }, proxy: false }),
|
|
293
|
-
nukeStoreAsRoot: (storeDir) => nukePathAsRoot(storeDir),
|
|
293
|
+
nukeStoreAsRoot: (storeDir) => nukePathAsRoot(storeDir, TEST_TMP_BASE),
|
|
294
294
|
storeExists: (storeDir) => existsSync(storeDir),
|
|
295
295
|
ensureNetwork: () => ensureRunnerOnNetwork(),
|
|
296
296
|
supportsNoPublish: ctlSupportsNoPublish,
|
package/package.json
CHANGED
package/root-nuke.d.ts
CHANGED
|
@@ -3,12 +3,20 @@ export type NukeSpawn = (cmd: string, argv: string[], opts?: unknown) => {
|
|
|
3
3
|
error?: unknown;
|
|
4
4
|
status?: number | null;
|
|
5
5
|
};
|
|
6
|
-
/** Refuse a target that would turn the nuke into something catastrophic.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
6
|
+
/** Refuse a target that would turn the nuke into something catastrophic.
|
|
7
|
+
*
|
|
8
|
+
* `allowedBase` is the containment: the target must sit strictly INSIDE it.
|
|
9
|
+
* Nothing else here is sufficient on its own, because the worst case passes
|
|
10
|
+
* every structural check. Under `--daemon reuse` the harness's storeDir is the
|
|
11
|
+
* developer's real `~/.norsk-ctl` -- absolute, ordinary leaf, no glob,
|
|
12
|
+
* non-root parent -- and nuking it mounts $HOME into a root container and
|
|
13
|
+
* removes the daemon's entire state: config, products, templates, instances.
|
|
14
|
+
* Today only an early return in teardown keeps the nuke away from it, and a
|
|
15
|
+
* guard that depends on a `return` staying put is not a guard.
|
|
16
|
+
*
|
|
17
|
+
* The base itself is refused too: it is shared by concurrently running tiers,
|
|
18
|
+
* so removing it would delete sibling runs' live stores. */
|
|
19
|
+
export declare function assertNukeTarget(target: string, allowedBase: string): void;
|
|
12
20
|
/** Docker argv for the nuke. Two things matter here:
|
|
13
21
|
*
|
|
14
22
|
* 1. The PARENT is mounted and the leaf removed by name -- mounting the target
|
|
@@ -23,7 +31,7 @@ export declare function nukeArgv(target: string): string[];
|
|
|
23
31
|
* reports a missing docker in its RESULT rather than throwing, and a caller
|
|
24
32
|
* that retries a doomed spawn forever (silently) is the failure mode to avoid.
|
|
25
33
|
* Throws only for an unsafe target -- see {@link assertNukeTarget}. */
|
|
26
|
-
export declare function nukePathAsRoot(target: string, spawn?: NukeSpawn): boolean;
|
|
34
|
+
export declare function nukePathAsRoot(target: string, allowedBase: string, spawn?: NukeSpawn): boolean;
|
|
27
35
|
export interface OwnerScanDeps {
|
|
28
36
|
readdir: (path: string) => string[];
|
|
29
37
|
isDir: (path: string) => boolean;
|
package/root-nuke.js
CHANGED
|
@@ -7,27 +7,44 @@
|
|
|
7
7
|
// prevention half.
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { lstatSync, readdirSync } from "node:fs";
|
|
10
|
-
import { basename, dirname, isAbsolute } from "node:path";
|
|
10
|
+
import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
|
|
11
11
|
const NUKE_IMAGE = "alpine:3";
|
|
12
12
|
const NUKE_TIMEOUT_MS = 60_000;
|
|
13
|
-
/** Refuse a target that would turn the nuke into something catastrophic.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
13
|
+
/** Refuse a target that would turn the nuke into something catastrophic.
|
|
14
|
+
*
|
|
15
|
+
* `allowedBase` is the containment: the target must sit strictly INSIDE it.
|
|
16
|
+
* Nothing else here is sufficient on its own, because the worst case passes
|
|
17
|
+
* every structural check. Under `--daemon reuse` the harness's storeDir is the
|
|
18
|
+
* developer's real `~/.norsk-ctl` -- absolute, ordinary leaf, no glob,
|
|
19
|
+
* non-root parent -- and nuking it mounts $HOME into a root container and
|
|
20
|
+
* removes the daemon's entire state: config, products, templates, instances.
|
|
21
|
+
* Today only an early return in teardown keeps the nuke away from it, and a
|
|
22
|
+
* guard that depends on a `return` staying put is not a guard.
|
|
23
|
+
*
|
|
24
|
+
* The base itself is refused too: it is shared by concurrently running tiers,
|
|
25
|
+
* so removing it would delete sibling runs' live stores. */
|
|
26
|
+
export function assertNukeTarget(target, allowedBase) {
|
|
19
27
|
if (!isAbsolute(target))
|
|
20
28
|
throw new Error(`refusing to nuke a relative path: ${target}`);
|
|
21
29
|
const leaf = basename(target);
|
|
22
30
|
if (leaf === "" || leaf === "." || leaf === "..")
|
|
23
31
|
throw new Error(`refusing to nuke a path with no leaf: ${target}`);
|
|
24
|
-
if (leaf
|
|
32
|
+
if (leaf.includes("*"))
|
|
25
33
|
throw new Error(`refusing to nuke a glob: ${target}`);
|
|
26
34
|
// dirname is what gets bind-mounted; "/" would hand the whole host to a root
|
|
27
|
-
// container. Every real target (a store under
|
|
35
|
+
// container. Every real target (a store under the temp base) is nested deeper.
|
|
28
36
|
if (dirname(target) === "/" || dirname(target) === target) {
|
|
29
37
|
throw new Error(`refusing to nuke a top-level path (its parent would be the mount): ${target}`);
|
|
30
38
|
}
|
|
39
|
+
// resolve() first so `..` cannot walk out, and compare with a trailing
|
|
40
|
+
// separator so `/x/test-temp-evil` does not pass as inside `/x/test-temp`.
|
|
41
|
+
const t = resolve(target);
|
|
42
|
+
const base = resolve(allowedBase);
|
|
43
|
+
if (t === base)
|
|
44
|
+
throw new Error(`refusing to nuke the temp base itself (concurrent runs live here): ${t}`);
|
|
45
|
+
if (!t.startsWith(base.endsWith(sep) ? base : base + sep)) {
|
|
46
|
+
throw new Error(`refusing to nuke ${t}: outside the test temp base ${base}`);
|
|
47
|
+
}
|
|
31
48
|
}
|
|
32
49
|
/** Docker argv for the nuke. Two things matter here:
|
|
33
50
|
*
|
|
@@ -59,8 +76,8 @@ export function nukeArgv(target) {
|
|
|
59
76
|
* reports a missing docker in its RESULT rather than throwing, and a caller
|
|
60
77
|
* that retries a doomed spawn forever (silently) is the failure mode to avoid.
|
|
61
78
|
* Throws only for an unsafe target -- see {@link assertNukeTarget}. */
|
|
62
|
-
export function nukePathAsRoot(target, spawn = spawnSync) {
|
|
63
|
-
assertNukeTarget(target);
|
|
79
|
+
export function nukePathAsRoot(target, allowedBase, spawn = spawnSync) {
|
|
80
|
+
assertNukeTarget(target, allowedBase);
|
|
64
81
|
const r = spawn("docker", nukeArgv(target), { timeout: NUKE_TIMEOUT_MS });
|
|
65
82
|
return !r?.error;
|
|
66
83
|
}
|
package/smoke.js
CHANGED
|
@@ -28,7 +28,7 @@ import { pollUntil } from "./poll.js";
|
|
|
28
28
|
import { nukePathAsRoot, reportForeignOwners } from "./root-nuke.js";
|
|
29
29
|
import { startSrtSources } from "./source-pump.js";
|
|
30
30
|
import { applyTestHost, fetchComponentState, fetchComponents, fetchStreamMappings, isSrtListenerState, } from "./studio-state.js";
|
|
31
|
-
import { makeStoreDir } from "./temp-dir.js";
|
|
31
|
+
import { makeStoreDir, TEST_TMP_BASE } from "./temp-dir.js";
|
|
32
32
|
const SMOKE_PORT_BASE = 33000;
|
|
33
33
|
const SMOKE_BAND_WIDTH = 20;
|
|
34
34
|
const SMOKE_BANDS = 50;
|
|
@@ -62,7 +62,7 @@ export const defaultSmokeDeps = {
|
|
|
62
62
|
fetch: (url) => fetch(url, { signal: AbortSignal.timeout(5000) }),
|
|
63
63
|
},
|
|
64
64
|
cleanup: (opts) => cleanupDaemon({ ...opts, stopProxy: async () => { }, proxy: false }),
|
|
65
|
-
nukeStoreAsRoot: (storeDir) => nukePathAsRoot(storeDir),
|
|
65
|
+
nukeStoreAsRoot: (storeDir) => nukePathAsRoot(storeDir, TEST_TMP_BASE),
|
|
66
66
|
storeExists: (storeDir) => existsSync(storeDir),
|
|
67
67
|
ensureNetwork: () => ensureRunnerOnNetwork(),
|
|
68
68
|
supportsNoPublish: ctlSupportsNoPublish,
|
package/temp-dir.js
CHANGED