@norskvideo/ctl-test-harness 0.1.38 → 0.1.39

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.
@@ -88,7 +88,33 @@ export declare function studioBaseFrom(studio: StudioReach, publishHost: string,
88
88
  * In a container the id also appears as the hostname, but cgroup is present in
89
89
  * both cgroup v1 (`/docker/<id>`) and v2 (`docker-<id>.scope`) layouts. */
90
90
  export declare function parseOwnContainerId(cgroupText: string): string | null;
91
- export declare function ownContainerId(read?: () => string): string | null;
91
+ /** This process's own container id, or null on a bare host.
92
+ *
93
+ * The cgroup id is preferred and costs nothing, but it is not reliable: under
94
+ * cgroup NAMESPACES — docker's default for years — a container sees `0::/` and
95
+ * no id at all, so cgroup alone reports "not in a container" from inside one.
96
+ * That false negative is why `ensureRunnerOnNetwork` has been attaching nothing
97
+ * on every docker-outside-of-docker runner while its caller read the verdict as
98
+ * success.
99
+ *
100
+ * The fallback does not guess at the hostname's shape. `/.dockerenv` answers
101
+ * "is THIS process containerised", which is the question, and `docker inspect`
102
+ * arbitrates what the hostname names — docker's default hostname is the short
103
+ * id, and compose sets it to the service name, which inspect resolves as well.
104
+ * Both must agree, so a bare host that merely shares a name with a container
105
+ * still resolves to null. */
106
+ export declare function resolveContainerId(opts: {
107
+ cgroupText: string | null;
108
+ inContainer: boolean;
109
+ hostname: string;
110
+ isDockerObject: (id: string) => boolean;
111
+ }): string | null;
112
+ export declare function ownContainerId(opts?: {
113
+ read?: () => string;
114
+ inContainer?: boolean;
115
+ hostname?: string;
116
+ isDockerObject?: (id: string) => boolean;
117
+ }): string | null;
92
118
  /** `docker network connect` is not idempotent — a second attach errors. Treat
93
119
  * the "already attached" variants as success so setup can call this blindly. */
94
120
  export declare function isAlreadyOnNetworkError(stderr: string): boolean;
@@ -103,8 +129,12 @@ type Runner = (cmd: string, args: string[]) => RunResult;
103
129
  * `failed` (a real error the caller should surface). */
104
130
  export declare function ensureRunnerOnNetwork(opts?: {
105
131
  network?: string;
106
- /** Explicit id (tests). Omitted -> read from /proc/self/cgroup. `null` forces the not-in-container branch. */
132
+ /** Explicit id (tests). Omitted -> resolved by `ownContainerId`. `null` forces the not-in-container branch. */
107
133
  containerId?: string | null;
134
+ /** Leaf inputs for that resolution (tests), so the whole chain can be
135
+ * exercised without docker. Omitted -> the real cgroup, /.dockerenv,
136
+ * hostname and `docker inspect`. */
137
+ detect?: Parameters<typeof ownContainerId>[0];
108
138
  run?: Runner;
109
139
  }): "attached" | "already" | "not-in-container" | "failed";
110
140
  export {};
package/container-net.js CHANGED
@@ -30,7 +30,8 @@
30
30
  // switch: `direct` on the Linux DooD CI box, `publish` (the default) everywhere
31
31
  // else. Nothing here changes behaviour until a caller opts a suite into `direct`.
32
32
  import { spawnSync } from "node:child_process";
33
- import { readFileSync } from "node:fs";
33
+ import { existsSync, readFileSync } from "node:fs";
34
+ import { hostname } from "node:os";
34
35
  // Mirrors of norsk-ctl's @norsk-ctl/shared constants — re-declared (not imported)
35
36
  // because this package doesn't depend on the ctl backend, exactly as
36
37
  // studio-state.ts re-declares MEDIA_HTTP_PORT. Source of truth:
@@ -136,13 +137,47 @@ export function parseOwnContainerId(cgroupText) {
136
137
  const m = cgroupText.match(/[0-9a-f]{64}/);
137
138
  return m ? m[0] : null;
138
139
  }
139
- export function ownContainerId(read = () => readFileSync("/proc/self/cgroup", "utf-8")) {
140
+ /** This process's own container id, or null on a bare host.
141
+ *
142
+ * The cgroup id is preferred and costs nothing, but it is not reliable: under
143
+ * cgroup NAMESPACES — docker's default for years — a container sees `0::/` and
144
+ * no id at all, so cgroup alone reports "not in a container" from inside one.
145
+ * That false negative is why `ensureRunnerOnNetwork` has been attaching nothing
146
+ * on every docker-outside-of-docker runner while its caller read the verdict as
147
+ * success.
148
+ *
149
+ * The fallback does not guess at the hostname's shape. `/.dockerenv` answers
150
+ * "is THIS process containerised", which is the question, and `docker inspect`
151
+ * arbitrates what the hostname names — docker's default hostname is the short
152
+ * id, and compose sets it to the service name, which inspect resolves as well.
153
+ * Both must agree, so a bare host that merely shares a name with a container
154
+ * still resolves to null. */
155
+ export function resolveContainerId(opts) {
156
+ const fromCgroup = opts.cgroupText === null ? null : parseOwnContainerId(opts.cgroupText);
157
+ if (fromCgroup)
158
+ return fromCgroup;
159
+ if (!opts.inContainer || !opts.hostname)
160
+ return null;
161
+ return opts.isDockerObject(opts.hostname) ? opts.hostname : null;
162
+ }
163
+ function dockerKnows(id) {
164
+ return spawnSync("docker", ["inspect", "-f", "{{.Id}}", id], { encoding: "utf-8" }).status === 0;
165
+ }
166
+ export function ownContainerId(opts = {}) {
167
+ const read = opts.read ?? (() => readFileSync("/proc/self/cgroup", "utf-8"));
168
+ let cgroupText;
140
169
  try {
141
- return parseOwnContainerId(read());
170
+ cgroupText = read();
142
171
  }
143
172
  catch {
144
- return null;
173
+ cgroupText = null;
145
174
  }
175
+ return resolveContainerId({
176
+ cgroupText,
177
+ inContainer: opts.inContainer ?? existsSync("/.dockerenv"),
178
+ hostname: opts.hostname ?? hostname(),
179
+ isDockerObject: opts.isDockerObject ?? dockerKnows,
180
+ });
146
181
  }
147
182
  /** `docker network connect` is not idempotent — a second attach errors. Treat
148
183
  * the "already attached" variants as success so setup can call this blindly. */
@@ -159,7 +194,7 @@ const defaultRunner = (cmd, args) => {
159
194
  * `failed` (a real error the caller should surface). */
160
195
  export function ensureRunnerOnNetwork(opts = {}) {
161
196
  const network = opts.network ?? DOCKER_NETWORK_NAME;
162
- const id = opts.containerId === undefined ? ownContainerId() : opts.containerId;
197
+ const id = opts.containerId === undefined ? ownContainerId(opts.detect) : opts.containerId;
163
198
  if (!id)
164
199
  return "not-in-container";
165
200
  const run = opts.run ?? defaultRunner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/smoke.js CHANGED
@@ -235,8 +235,16 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
235
235
  const started = deps.startDaemon(storeDir, { port: ports.daemonPort, seedConfig: false });
236
236
  daemon = started.daemon;
237
237
  await started.ready;
238
- if (mode === "direct" && deps.ensureNetwork() === "failed") {
239
- throw new Error("could not join the runner to norsk-net for direct reach");
238
+ if (mode === "direct") {
239
+ // Say which verdict it was. "not-in-container" used to be the SILENT
240
+ // outcome on every docker-outside-of-docker runner, because the cgroup-only
241
+ // detection could not see through a cgroup namespace; direct reach then
242
+ // worked or not by luck of the runner already being on norsk-net, and
243
+ // nothing in the log distinguished the two.
244
+ const verdict = deps.ensureNetwork();
245
+ deps.log(`runner network for direct reach: ${verdict}`);
246
+ if (verdict === "failed")
247
+ throw new Error("could not join the runner to norsk-net for direct reach");
240
248
  }
241
249
  // Register.
242
250
  const licenseFile = spec.product.licenseFile ?? deps.licenseFile();