@norskvideo/ctl-sdk 0.1.48 → 0.1.50
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/package.json +1 -1
- package/product-error.d.ts +6 -2
- package/product-reach.d.ts +2 -0
- package/product-reach.js +18 -5
- package/product-service.d.ts +10 -0
- package/product-service.js +15 -1
package/package.json
CHANGED
package/product-error.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export declare class ProductError extends Error {
|
|
2
2
|
code: "DEV_URL_INVALID" | "DEV_URL_NOT_LOCALHOST" | "PORT_EXHAUSTED" | "DOCKER_PULL_FAILED" | "DOCKER_RUN_FAILED" | "READINESS_TIMEOUT" | "MANIFEST_FETCH_FAILED" | "MANIFEST_INVALID" | "CONFIG_SCREEN_UNREACHABLE" | "CONFIG_SCREEN_DEV_SERVER" | "LICENSE_INVALID" | "NAME_CONFLICT" | "NOT_FOUND" | "NOT_RESTARTABLE" | "RUNNER_TOO_OLD" | "PRODUCT_TEMPLATE_FETCH_FAILED"
|
|
3
3
|
/** Two products declare one bare role name with different `defaultFor`. */
|
|
4
|
-
| "ROLE_CONFLICT"
|
|
4
|
+
| "ROLE_CONFLICT"
|
|
5
|
+
/** The host's custom role names could not be read to check for a clash. */
|
|
6
|
+
| "ROLE_NAMES_UNREADABLE";
|
|
5
7
|
constructor(code: "DEV_URL_INVALID" | "DEV_URL_NOT_LOCALHOST" | "PORT_EXHAUSTED" | "DOCKER_PULL_FAILED" | "DOCKER_RUN_FAILED" | "READINESS_TIMEOUT" | "MANIFEST_FETCH_FAILED" | "MANIFEST_INVALID" | "CONFIG_SCREEN_UNREACHABLE" | "CONFIG_SCREEN_DEV_SERVER" | "LICENSE_INVALID" | "NAME_CONFLICT" | "NOT_FOUND" | "NOT_RESTARTABLE" | "RUNNER_TOO_OLD" | "PRODUCT_TEMPLATE_FETCH_FAILED"
|
|
6
8
|
/** Two products declare one bare role name with different `defaultFor`. */
|
|
7
|
-
| "ROLE_CONFLICT"
|
|
9
|
+
| "ROLE_CONFLICT"
|
|
10
|
+
/** The host's custom role names could not be read to check for a clash. */
|
|
11
|
+
| "ROLE_NAMES_UNREADABLE", message: string);
|
|
8
12
|
}
|
package/product-reach.d.ts
CHANGED
|
@@ -5,5 +5,7 @@ export type ProductReach = "host" | "container-address";
|
|
|
5
5
|
export declare function parseContainerIdFromCgroup(text: string): string | null;
|
|
6
6
|
export declare function detectProductReach(opts: {
|
|
7
7
|
cgroup?: () => string;
|
|
8
|
+
/** Whether /.dockerenv exists; injectable so a test does not read the host it runs on. */
|
|
9
|
+
dockerenv?: () => boolean;
|
|
8
10
|
env?: Record<string, string | undefined>;
|
|
9
11
|
}): ProductReach;
|
package/product-reach.js
CHANGED
|
@@ -5,11 +5,21 @@
|
|
|
5
5
|
// host, not to the daemon, and every registration times out at readiness. The
|
|
6
6
|
// container's own bridge address on the internal port is what such a daemon
|
|
7
7
|
// can dial, so a containerised daemon uses that and remembers it on the
|
|
8
|
-
// registration (`reachHost`). Detection reads the daemon's own cgroup,
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// registration (`reachHost`). Detection reads the daemon's own cgroup, then
|
|
9
|
+
// /.dockerenv; NORSK_CTL_PRODUCT_REACH overrides both.
|
|
10
|
+
//
|
|
11
|
+
// The cgroup alone is not enough. Under a cgroup NAMESPACE (docker's default on
|
|
12
|
+
// cgroup v2) a container sees `0::/` and no id, so it reads as a bare host from
|
|
13
|
+
// inside a container. That sent funke's image-mode demo check on an arm DooD
|
|
14
|
+
// runner to 127.0.0.1:<host port> on 2026-10-04, which never answers. The test
|
|
15
|
+
// harness fixed the same false negative in its own resolveContainerId; this is
|
|
16
|
+
// the daemon's copy of that fix. /.dockerenv is written by the docker daemon
|
|
17
|
+
// into every container it creates, and answers the question asked here: is
|
|
18
|
+
// THIS process containerised.
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
20
|
const CGROUP_PATH = "/proc/self/cgroup";
|
|
12
21
|
const REACH_ENV = "NORSK_CTL_PRODUCT_REACH";
|
|
22
|
+
const DOCKERENV_PATH = "/.dockerenv";
|
|
13
23
|
/** The 64-hex docker container id from a /proc/self/cgroup dump, or null on a
|
|
14
24
|
* bare host. Present in both cgroup v1 (`/docker/<id>`) and v2
|
|
15
25
|
* (`docker-<id>.scope`) layouts. */
|
|
@@ -23,10 +33,13 @@ export function detectProductReach(opts) {
|
|
|
23
33
|
if (forced === "host" || forced === "container-address")
|
|
24
34
|
return forced;
|
|
25
35
|
const read = opts.cgroup ?? (() => readFileSync(CGROUP_PATH, "utf-8"));
|
|
36
|
+
const dockerenv = opts.dockerenv ?? (() => existsSync(DOCKERENV_PATH));
|
|
26
37
|
try {
|
|
27
|
-
|
|
38
|
+
if (parseContainerIdFromCgroup(read()))
|
|
39
|
+
return "container-address";
|
|
28
40
|
}
|
|
29
41
|
catch {
|
|
30
|
-
|
|
42
|
+
// No /proc: fall through to the marker file.
|
|
31
43
|
}
|
|
44
|
+
return dockerenv() ? "container-address" : "host";
|
|
32
45
|
}
|
package/product-service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Result } from "@norskvideo/ctl-foundation";
|
|
1
2
|
import { type ProductPlacement } from "./docker-runner.js";
|
|
2
3
|
import { type LicenseStager } from "./license-registration.js";
|
|
3
4
|
import type { Manifest, ProductRoleDeclaration } from "./manifest-schema.js";
|
|
@@ -159,6 +160,14 @@ export interface ProductServiceOptions {
|
|
|
159
160
|
onRolesDeclared?: (productName: string, roles: readonly ProductRoleDeclaration[], acceptance: {
|
|
160
161
|
pending: readonly RoleDefault[];
|
|
161
162
|
}) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Role names a product may not declare, because the host already uses them
|
|
165
|
+
* for something else: its custom roles (ADR-0014). Names are one name space
|
|
166
|
+
* on a host, so a product role spelt like a custom role would be ambiguous in
|
|
167
|
+
* every assignment naming it. Omitted means none. An error is a host that
|
|
168
|
+
* cannot say which names it uses, and refuses any product declaring roles.
|
|
169
|
+
*/
|
|
170
|
+
reservedRoleNames?: () => Result<ReadonlySet<string>, string>;
|
|
162
171
|
/** Told once a registration is removed, so the host can drop whatever it
|
|
163
172
|
* holds against the product (role assignments on `product/<name>`). */
|
|
164
173
|
onRemoved?: (productName: string) => void;
|
|
@@ -219,6 +228,7 @@ export declare class ProductService {
|
|
|
219
228
|
private reachHostFor;
|
|
220
229
|
private readonly onRolesDeclared?;
|
|
221
230
|
private readonly onRemoved?;
|
|
231
|
+
private readonly reservedRoleNames?;
|
|
222
232
|
/**
|
|
223
233
|
* Product roles travel bare, so a name means one thing on a host. Two
|
|
224
234
|
* products may share one — a common vocabulary is the point — but not with
|
package/product-service.js
CHANGED
|
@@ -74,6 +74,7 @@ export class ProductService {
|
|
|
74
74
|
this.knownCapabilities = opts.knownCapabilities ?? [];
|
|
75
75
|
this.identityEnvFor = opts.identityEnvFor;
|
|
76
76
|
this.onRolesDeclared = opts.onRolesDeclared;
|
|
77
|
+
this.reservedRoleNames = opts.reservedRoleNames;
|
|
77
78
|
this.onRemoved = opts.onRemoved;
|
|
78
79
|
this.forgetIdentity = opts.forgetIdentity;
|
|
79
80
|
this.probeHeadersFor = opts.probeHeadersFor;
|
|
@@ -98,6 +99,7 @@ export class ProductService {
|
|
|
98
99
|
}
|
|
99
100
|
onRolesDeclared;
|
|
100
101
|
onRemoved;
|
|
102
|
+
reservedRoleNames;
|
|
101
103
|
/**
|
|
102
104
|
* Product roles travel bare, so a name means one thing on a host. Two
|
|
103
105
|
* products may share one — a common vocabulary is the point — but not with
|
|
@@ -106,7 +108,19 @@ export class ProductService {
|
|
|
106
108
|
* know which two products to talk to.
|
|
107
109
|
*/
|
|
108
110
|
refuseRoleConflicts(manifest, others) {
|
|
109
|
-
|
|
111
|
+
const declared = manifest.access?.roles ?? [];
|
|
112
|
+
const read = declared.length === 0 ? undefined : this.reservedRoleNames?.();
|
|
113
|
+
// Fail closed: a clash nobody could see would make one name mean two roles.
|
|
114
|
+
if (read?.status === "error") {
|
|
115
|
+
return new ProductError("ROLE_NAMES_UNREADABLE", `'${manifest.name}' declares product roles, and this host's custom roles cannot be read to check they ` +
|
|
116
|
+
`do not clash: ${read.error}`);
|
|
117
|
+
}
|
|
118
|
+
const reserved = read?.status === "ok" ? read.value : new Set();
|
|
119
|
+
for (const role of declared) {
|
|
120
|
+
if (reserved.has(role.name)) {
|
|
121
|
+
return new ProductError("ROLE_CONFLICT", `product role '${role.name}' is already a custom role on this host; role names are one name space, ` +
|
|
122
|
+
"so rename the custom role (or remove it) before registering this product");
|
|
123
|
+
}
|
|
110
124
|
for (const other of others) {
|
|
111
125
|
if (other.name === manifest.name)
|
|
112
126
|
continue;
|