@norskvideo/ctl-sdk 0.1.20 → 0.1.21
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/docker-runner.d.ts +4 -0
- package/docker-runner.js +11 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/manifest-fetch.d.ts +9 -2
- package/manifest-fetch.js +13 -1
- package/package.json +1 -1
- package/product-health-monitor.js +2 -2
- package/product-reach.d.ts +9 -0
- package/product-reach.js +32 -0
- package/product-service.d.ts +14 -0
- package/product-service.js +44 -17
- package/product-types.d.ts +4 -0
- package/proxy-middleware.js +2 -1
package/docker-runner.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const CONTAINER_INTERNAL_PORT = 4321;
|
|
1
2
|
/** Role label stamped on every product control-plane container. The daemon's
|
|
2
3
|
* infrastructure view discovers managed singletons by the presence of a
|
|
3
4
|
* `norsk-ctl.role` label (proxy, proxy-oauth2, cpu-monitor, product); the
|
|
@@ -16,6 +17,9 @@ export declare function productContainerName(productName: string): string;
|
|
|
16
17
|
export declare function isDigestRef(image: string): boolean;
|
|
17
18
|
export declare function dockerPull(image: string): Promise<void>;
|
|
18
19
|
export declare function dockerRun(image: string, hostPort: number): Promise<string>;
|
|
20
|
+
/** The container's address on its first network (the default bridge for a
|
|
21
|
+
* product container), or undefined when docker cannot say. */
|
|
22
|
+
export declare function dockerAddress(containerId: string): Promise<string | undefined>;
|
|
19
23
|
export declare function dockerRm(containerId: string): Promise<void>;
|
|
20
24
|
/** Best-effort rename of a running container. The product is tracked by
|
|
21
25
|
* container id, never by name, so a rename failure (e.g. a stale container
|
package/docker-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProductError } from "./product-error.js";
|
|
2
|
-
const CONTAINER_INTERNAL_PORT = 4321;
|
|
2
|
+
export const CONTAINER_INTERNAL_PORT = 4321;
|
|
3
3
|
/** Role label stamped on every product control-plane container. The daemon's
|
|
4
4
|
* infrastructure view discovers managed singletons by the presence of a
|
|
5
5
|
* `norsk-ctl.role` label (proxy, proxy-oauth2, cpu-monitor, product); the
|
|
@@ -53,6 +53,16 @@ export async function dockerRun(image, hostPort) {
|
|
|
53
53
|
}
|
|
54
54
|
return stdout;
|
|
55
55
|
}
|
|
56
|
+
/** The container's address on its first network (the default bridge for a
|
|
57
|
+
* product container), or undefined when docker cannot say. */
|
|
58
|
+
export async function dockerAddress(containerId) {
|
|
59
|
+
const proc = Bun.spawn(["docker", "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}", containerId], { stdout: "pipe", stderr: "pipe" });
|
|
60
|
+
const exitCode = await proc.exited;
|
|
61
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
62
|
+
if (exitCode !== 0 || !stdout)
|
|
63
|
+
return undefined;
|
|
64
|
+
return stdout.split(/\s+/)[0];
|
|
65
|
+
}
|
|
56
66
|
export async function dockerRm(containerId) {
|
|
57
67
|
const proc = Bun.spawn(["docker", "rm", "-f", containerId], { stdout: "pipe", stderr: "pipe" });
|
|
58
68
|
await proc.exited;
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./manifest-fetch.js";
|
|
|
12
12
|
export * from "./manifest-router.js";
|
|
13
13
|
export * from "./openapi-router.js";
|
|
14
14
|
export * from "./product-health-monitor.js";
|
|
15
|
+
export * from "./product-reach.js";
|
|
15
16
|
export * from "./product-service.js";
|
|
16
17
|
export * from "./product-template-materials.js";
|
|
17
18
|
export * from "./proxy-middleware.js";
|
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./manifest-fetch.js";
|
|
|
14
14
|
export * from "./manifest-router.js";
|
|
15
15
|
export * from "./openapi-router.js";
|
|
16
16
|
export * from "./product-health-monitor.js";
|
|
17
|
+
export * from "./product-reach.js";
|
|
17
18
|
export * from "./product-service.js";
|
|
18
19
|
export * from "./product-template-materials.js";
|
|
19
20
|
export * from "./proxy-middleware.js";
|
package/manifest-fetch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Manifest } from "./manifest-schema.js";
|
|
2
|
-
import type { ProductSpec } from "./product-types.js";
|
|
2
|
+
import type { ProductRegistration, ProductSpec } from "./product-types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Fast liveness probe for a registered product. A dev-mode product is
|
|
5
5
|
* externally owned, so the only way to know it's actually up is to ask it.
|
|
@@ -8,7 +8,14 @@ import type { ProductSpec } from "./product-types.js";
|
|
|
8
8
|
* because it runs on the product-list render path.
|
|
9
9
|
*/
|
|
10
10
|
export declare function isDevUrlAlive(baseUrl: string): Promise<boolean>;
|
|
11
|
-
|
|
11
|
+
/** Where the daemon dials a product. A container product is loopback-published
|
|
12
|
+
* on `port`; a daemon that itself runs in a container cannot reach that
|
|
13
|
+
* loopback and dials the container's own address (`reachHost`) on the internal
|
|
14
|
+
* port instead — see product-reach.ts. */
|
|
15
|
+
export declare function specBaseUrl(spec: ProductSpec, port?: number, reachHost?: string): string;
|
|
16
|
+
/** specBaseUrl for a registration — the one call every dial site should make,
|
|
17
|
+
* so a recorded reachHost is never forgotten. */
|
|
18
|
+
export declare function productBaseUrl(reg: Pick<ProductRegistration, "spec" | "port" | "reachHost">): string;
|
|
12
19
|
export declare function fetchManifest(baseUrl: string): Promise<Manifest>;
|
|
13
20
|
export declare function waitForReady(baseUrl: string): Promise<void>;
|
|
14
21
|
/**
|
package/manifest-fetch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CONTAINER_INTERNAL_PORT } from "./docker-runner.js";
|
|
1
2
|
import { ManifestSchema } from "./manifest-schema.js";
|
|
2
3
|
import { ProductError } from "./product-error.js";
|
|
3
4
|
const READINESS_TIMEOUT_MS = 60_000;
|
|
@@ -19,13 +20,24 @@ export async function isDevUrlAlive(baseUrl) {
|
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
/** Where the daemon dials a product. A container product is loopback-published
|
|
24
|
+
* on `port`; a daemon that itself runs in a container cannot reach that
|
|
25
|
+
* loopback and dials the container's own address (`reachHost`) on the internal
|
|
26
|
+
* port instead — see product-reach.ts. */
|
|
27
|
+
export function specBaseUrl(spec, port, reachHost) {
|
|
23
28
|
if (spec.kind === "dev")
|
|
24
29
|
return spec.url.replace(/\/$/, "");
|
|
30
|
+
if (reachHost !== undefined)
|
|
31
|
+
return `http://${reachHost}:${CONTAINER_INTERNAL_PORT}`;
|
|
25
32
|
if (port === undefined)
|
|
26
33
|
throw new Error("container spec requires port");
|
|
27
34
|
return `http://127.0.0.1:${port}`;
|
|
28
35
|
}
|
|
36
|
+
/** specBaseUrl for a registration — the one call every dial site should make,
|
|
37
|
+
* so a recorded reachHost is never forgotten. */
|
|
38
|
+
export function productBaseUrl(reg) {
|
|
39
|
+
return specBaseUrl(reg.spec, reg.port, reg.reachHost);
|
|
40
|
+
}
|
|
29
41
|
export async function fetchManifest(baseUrl) {
|
|
30
42
|
let response;
|
|
31
43
|
try {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "@norskvideo/ctl-foundation";
|
|
2
|
-
import {
|
|
2
|
+
import { productBaseUrl } from "./manifest-fetch.js";
|
|
3
3
|
const UNKNOWN = { status: "unknown", consecutiveFailures: 0, restartAttempts: 0 };
|
|
4
4
|
/**
|
|
5
5
|
* Periodically probes each container product's health endpoint and, after
|
|
@@ -123,7 +123,7 @@ export async function probeProductHealth(reg, timeoutMs = 3_000) {
|
|
|
123
123
|
if (reg.spec.kind !== "container" || reg.port === undefined)
|
|
124
124
|
return false;
|
|
125
125
|
const path = reg.manifest.api?.healthCheckPath ?? "/healthz";
|
|
126
|
-
const base =
|
|
126
|
+
const base = productBaseUrl(reg);
|
|
127
127
|
try {
|
|
128
128
|
const r = await fetch(`${base}${path.startsWith("/") ? path : `/${path}`}`, {
|
|
129
129
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ProductReach = "host" | "container-address";
|
|
2
|
+
/** The 64-hex docker container id from a /proc/self/cgroup dump, or null on a
|
|
3
|
+
* bare host. Present in both cgroup v1 (`/docker/<id>`) and v2
|
|
4
|
+
* (`docker-<id>.scope`) layouts. */
|
|
5
|
+
export declare function parseContainerIdFromCgroup(text: string): string | null;
|
|
6
|
+
export declare function detectProductReach(opts: {
|
|
7
|
+
cgroup?: () => string;
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
9
|
+
}): ProductReach;
|
package/product-reach.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// How the daemon reaches a product's control-plane container. The container is
|
|
2
|
+
// loopback-published (`-p 127.0.0.1:<host port>:4321`), which is right when the
|
|
3
|
+
// daemon runs on the docker host. When the daemon itself runs in a container —
|
|
4
|
+
// a docker-outside-of-docker CI runner — that loopback belongs to the docker
|
|
5
|
+
// host, not to the daemon, and every registration times out at readiness. The
|
|
6
|
+
// container's own bridge address on the internal port is what such a daemon
|
|
7
|
+
// can dial, so a containerised daemon uses that and remembers it on the
|
|
8
|
+
// registration (`reachHost`). Detection reads the daemon's own cgroup, the
|
|
9
|
+
// same tell the test harness uses; NORSK_CTL_PRODUCT_REACH overrides it.
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
const CGROUP_PATH = "/proc/self/cgroup";
|
|
12
|
+
const REACH_ENV = "NORSK_CTL_PRODUCT_REACH";
|
|
13
|
+
/** The 64-hex docker container id from a /proc/self/cgroup dump, or null on a
|
|
14
|
+
* bare host. Present in both cgroup v1 (`/docker/<id>`) and v2
|
|
15
|
+
* (`docker-<id>.scope`) layouts. */
|
|
16
|
+
export function parseContainerIdFromCgroup(text) {
|
|
17
|
+
const m = text.match(/[0-9a-f]{64}/);
|
|
18
|
+
return m ? m[0] : null;
|
|
19
|
+
}
|
|
20
|
+
export function detectProductReach(opts) {
|
|
21
|
+
const env = opts.env ?? process.env;
|
|
22
|
+
const forced = env[REACH_ENV];
|
|
23
|
+
if (forced === "host" || forced === "container-address")
|
|
24
|
+
return forced;
|
|
25
|
+
const read = opts.cgroup ?? (() => readFileSync(CGROUP_PATH, "utf-8"));
|
|
26
|
+
try {
|
|
27
|
+
return parseContainerIdFromCgroup(read()) ? "container-address" : "host";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "host";
|
|
31
|
+
}
|
|
32
|
+
}
|
package/product-service.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LicenseStager } from "./license-registration.js";
|
|
2
|
+
import { type ProductReach } from "./product-reach.js";
|
|
2
3
|
import type { ProductTemplateSource } from "./product-template-record.js";
|
|
3
4
|
import type { ProductLicense, ProductRegistration, ProductSpec } from "./product-types.js";
|
|
4
5
|
/**
|
|
@@ -83,6 +84,10 @@ export interface ProductContainerOps {
|
|
|
83
84
|
remove(containerId: string): Promise<void>;
|
|
84
85
|
rename(containerId: string, name: string): Promise<void>;
|
|
85
86
|
waitForReady(baseUrl: string): Promise<void>;
|
|
87
|
+
/** The container's own address, consulted only under reach
|
|
88
|
+
* "container-address" (see product-reach.ts). Optional: a host without it
|
|
89
|
+
* falls back to the loopback host port. */
|
|
90
|
+
address?(containerId: string): Promise<string | undefined>;
|
|
86
91
|
}
|
|
87
92
|
/** The real docker-runner wiring. Exported so a host can override one op
|
|
88
93
|
* (typically `pull`, onto its own docker adapter) and keep the rest. */
|
|
@@ -107,6 +112,11 @@ export interface ProductServiceOptions {
|
|
|
107
112
|
/** Container lifecycle ops used by add/remove/restart/stopAll/restoreAll.
|
|
108
113
|
* Injectable for tests; defaults to the real docker-runner functions. */
|
|
109
114
|
containerOps?: ProductContainerOps;
|
|
115
|
+
/** How this daemon reaches a product container: the loopback host port
|
|
116
|
+
* (a host daemon) or the container's own address (a daemon that is itself
|
|
117
|
+
* in a container). Default: detected from the daemon's cgroup, overridable
|
|
118
|
+
* with NORSK_CTL_PRODUCT_REACH. */
|
|
119
|
+
reach?: ProductReach;
|
|
110
120
|
}
|
|
111
121
|
export declare class ProductService {
|
|
112
122
|
/**
|
|
@@ -131,7 +141,11 @@ export declare class ProductService {
|
|
|
131
141
|
private readonly stageLicense?;
|
|
132
142
|
private readonly isDevUrlAlive;
|
|
133
143
|
private readonly containerOps;
|
|
144
|
+
private readonly reach;
|
|
134
145
|
constructor(opts: ProductServiceOptions);
|
|
146
|
+
/** The address to record for a freshly started container: its own, when
|
|
147
|
+
* this daemon cannot dial the host loopback; otherwise nothing. */
|
|
148
|
+
private reachHostFor;
|
|
135
149
|
list(): ProductRegistration[];
|
|
136
150
|
/** Docker's default `--pull=missing` keeps whatever a moving tag resolved to
|
|
137
151
|
* last time, so a rebuilt `:latest`/`:dev` never reached a new registration.
|
package/product-service.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { logger, Mutex } from "@norskvideo/ctl-foundation";
|
|
3
3
|
import { validateDevUrl } from "./dev-url.js";
|
|
4
|
-
import { dockerPull, dockerRename, dockerRm, dockerRun, isDigestRef, productContainerName } from "./docker-runner.js";
|
|
4
|
+
import { dockerAddress, dockerPull, dockerRename, dockerRm, dockerRun, isDigestRef, productContainerName, } from "./docker-runner.js";
|
|
5
5
|
import { resolveLicenseFile } from "./license-registration.js";
|
|
6
6
|
import { checkProductEntitlement, notV2EnvelopeMessage, parseLicenseContents } from "./license-v2.js";
|
|
7
|
-
import { fetchManifest, isDevUrlAlive, probeConfigScreen, specBaseUrl, waitForReady } from "./manifest-fetch.js";
|
|
7
|
+
import { fetchManifest, isDevUrlAlive, probeConfigScreen, productBaseUrl, specBaseUrl, waitForReady, } from "./manifest-fetch.js";
|
|
8
8
|
import { ProductError } from "./product-error.js";
|
|
9
|
+
import { detectProductReach } from "./product-reach.js";
|
|
9
10
|
/** GET a product-template tar from a product. `url` is whatever the manifest
|
|
10
11
|
* declared; we treat it as a path on the product's HTTP surface and
|
|
11
12
|
* resolve against the product's base URL. Returns the raw tar bytes,
|
|
@@ -32,6 +33,7 @@ export const defaultProductContainerOps = {
|
|
|
32
33
|
remove: dockerRm,
|
|
33
34
|
rename: dockerRename,
|
|
34
35
|
waitForReady,
|
|
36
|
+
address: dockerAddress,
|
|
35
37
|
};
|
|
36
38
|
export class ProductService {
|
|
37
39
|
/**
|
|
@@ -56,6 +58,7 @@ export class ProductService {
|
|
|
56
58
|
stageLicense;
|
|
57
59
|
isDevUrlAlive;
|
|
58
60
|
containerOps;
|
|
61
|
+
reach;
|
|
59
62
|
constructor(opts) {
|
|
60
63
|
this.store = opts.store;
|
|
61
64
|
this.allocatePort = opts.allocatePort;
|
|
@@ -64,6 +67,14 @@ export class ProductService {
|
|
|
64
67
|
this.stageLicense = opts.stageLicense;
|
|
65
68
|
this.isDevUrlAlive = opts.isDevUrlAlive ?? isDevUrlAlive;
|
|
66
69
|
this.containerOps = opts.containerOps ?? defaultProductContainerOps;
|
|
70
|
+
this.reach = opts.reach ?? detectProductReach({});
|
|
71
|
+
}
|
|
72
|
+
/** The address to record for a freshly started container: its own, when
|
|
73
|
+
* this daemon cannot dial the host loopback; otherwise nothing. */
|
|
74
|
+
async reachHostFor(containerId) {
|
|
75
|
+
if (this.reach !== "container-address" || !this.containerOps.address)
|
|
76
|
+
return undefined;
|
|
77
|
+
return this.containerOps.address(containerId);
|
|
67
78
|
}
|
|
68
79
|
list() {
|
|
69
80
|
return this.store.read();
|
|
@@ -90,7 +101,7 @@ export class ProductService {
|
|
|
90
101
|
async isRunning(reg) {
|
|
91
102
|
if (reg.spec.kind === "container")
|
|
92
103
|
return reg.containerId !== undefined;
|
|
93
|
-
return this.isDevUrlAlive(
|
|
104
|
+
return this.isDevUrlAlive(productBaseUrl(reg));
|
|
94
105
|
}
|
|
95
106
|
async add(spec, opts = {}) {
|
|
96
107
|
return this.mutations.run(() => this.addSerialised(spec, opts));
|
|
@@ -100,6 +111,7 @@ export class ProductService {
|
|
|
100
111
|
let baseUrl;
|
|
101
112
|
let port;
|
|
102
113
|
let containerId;
|
|
114
|
+
let reachHost;
|
|
103
115
|
if (spec.kind === "dev") {
|
|
104
116
|
validateDevUrl(spec.url);
|
|
105
117
|
baseUrl = specBaseUrl(spec);
|
|
@@ -117,7 +129,8 @@ export class ProductService {
|
|
|
117
129
|
await this.refreshImage(spec.image);
|
|
118
130
|
logger.info(`Starting product container: ${spec.image} on host port ${port}`);
|
|
119
131
|
containerId = await this.containerOps.run(spec.image, port);
|
|
120
|
-
|
|
132
|
+
reachHost = await this.reachHostFor(containerId);
|
|
133
|
+
baseUrl = specBaseUrl(spec, port, reachHost);
|
|
121
134
|
}
|
|
122
135
|
try {
|
|
123
136
|
await this.containerOps.waitForReady(baseUrl);
|
|
@@ -221,6 +234,7 @@ export class ProductService {
|
|
|
221
234
|
manifest,
|
|
222
235
|
...(port !== undefined ? { port } : {}),
|
|
223
236
|
...(containerId !== undefined ? { containerId } : {}),
|
|
237
|
+
...(reachHost !== undefined ? { reachHost } : {}),
|
|
224
238
|
...(opts.license !== undefined
|
|
225
239
|
? {
|
|
226
240
|
license: opts.license.mode === "byol" && licenseFile !== undefined
|
|
@@ -304,12 +318,14 @@ export class ProductService {
|
|
|
304
318
|
}
|
|
305
319
|
}
|
|
306
320
|
const containerId = await this.containerOps.run(target.spec.image, target.port);
|
|
307
|
-
|
|
308
|
-
await this.
|
|
321
|
+
const reachHost = await this.reachHostFor(containerId);
|
|
322
|
+
await this.store.update((products) => products.map((p) => (p.name === name ? withContainerId(p, containerId, reachHost) : p)));
|
|
323
|
+
await this.containerOps.waitForReady(specBaseUrl(target.spec, target.port, reachHost));
|
|
309
324
|
await this.containerOps.rename(containerId, productContainerName(name));
|
|
310
325
|
containerRestarted = true;
|
|
311
326
|
}
|
|
312
|
-
const
|
|
327
|
+
const current = this.store.read().find((p) => p.name === name) ?? target;
|
|
328
|
+
const baseUrl = productBaseUrl(current);
|
|
313
329
|
const manifest = await fetchManifest(baseUrl);
|
|
314
330
|
if (manifest.name !== target.name) {
|
|
315
331
|
throw new ProductError("NAME_CONFLICT", `manifest now reports name '${manifest.name}', was '${target.name}' — use remove + add`);
|
|
@@ -393,9 +409,10 @@ export class ProductService {
|
|
|
393
409
|
}
|
|
394
410
|
try {
|
|
395
411
|
const containerId = await this.containerOps.run(reg.spec.image, reg.port);
|
|
396
|
-
await this.
|
|
412
|
+
const reachHost = await this.reachHostFor(containerId);
|
|
413
|
+
await this.containerOps.waitForReady(specBaseUrl(reg.spec, reg.port, reachHost));
|
|
397
414
|
await this.containerOps.rename(containerId, productContainerName(reg.name));
|
|
398
|
-
restored.set(reg.name, containerId);
|
|
415
|
+
restored.set(reg.name, { containerId, reachHost });
|
|
399
416
|
logger.info(`Product '${reg.name}' container restored on host port ${reg.port}`);
|
|
400
417
|
}
|
|
401
418
|
catch (e) {
|
|
@@ -403,7 +420,12 @@ export class ProductService {
|
|
|
403
420
|
logger.warn(`Product '${reg.name}': failed to restore — ${e instanceof Error ? e.message : String(e)}`);
|
|
404
421
|
}
|
|
405
422
|
}));
|
|
406
|
-
await this.store.update((products) => products.map((p) =>
|
|
423
|
+
await this.store.update((products) => products.map((p) => {
|
|
424
|
+
if (!restored.has(p.name))
|
|
425
|
+
return p;
|
|
426
|
+
const r = restored.get(p.name);
|
|
427
|
+
return withContainerId(p, r?.containerId, r?.reachHost);
|
|
428
|
+
}));
|
|
407
429
|
}
|
|
408
430
|
/** Stop (if still present) and relaunch a single container-kind product,
|
|
409
431
|
* recording the fresh containerId. Used by the health monitor to recover a
|
|
@@ -434,15 +456,20 @@ export class ProductService {
|
|
|
434
456
|
}
|
|
435
457
|
}
|
|
436
458
|
const containerId = await this.containerOps.run(target.spec.image, target.port);
|
|
437
|
-
|
|
438
|
-
await this.
|
|
459
|
+
const reachHost = await this.reachHostFor(containerId);
|
|
460
|
+
await this.store.update((products) => products.map((p) => (p.name === name ? withContainerId(p, containerId, reachHost) : p)));
|
|
461
|
+
await this.containerOps.waitForReady(specBaseUrl(target.spec, target.port, reachHost));
|
|
439
462
|
await this.containerOps.rename(containerId, productContainerName(name));
|
|
440
463
|
logger.info(`Product '${name}' container restarted on host port ${target.port}`);
|
|
441
464
|
}
|
|
442
465
|
}
|
|
443
|
-
/** Return a copy of `reg` with containerId
|
|
444
|
-
* preserving the "omit the key entirely"
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
466
|
+
/** Return a copy of `reg` with containerId (and the address it was reached at)
|
|
467
|
+
* set, or both removed when undefined, preserving the "omit the key entirely"
|
|
468
|
+
* shape the store round-trips. A stale reachHost would point the health
|
|
469
|
+
* monitor at a container that no longer exists, so it travels with the id. */
|
|
470
|
+
function withContainerId(reg, containerId, reachHost) {
|
|
471
|
+
const { containerId: _drop, reachHost: _dropHost, ...rest } = reg;
|
|
472
|
+
if (containerId === undefined)
|
|
473
|
+
return rest;
|
|
474
|
+
return { ...rest, containerId, ...(reachHost !== undefined ? { reachHost } : {}) };
|
|
448
475
|
}
|
package/product-types.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface ProductRegistration {
|
|
|
25
25
|
port?: number;
|
|
26
26
|
/** Docker container ID (container mode only). */
|
|
27
27
|
containerId?: string;
|
|
28
|
+
/** The container's own address, recorded when the daemon runs in a
|
|
29
|
+
* container and cannot dial the loopback-published host port (see
|
|
30
|
+
* product-reach.ts). Absent on a host daemon: 127.0.0.1:<port> is right. */
|
|
31
|
+
reachHost?: string;
|
|
28
32
|
/** Absent on registrations that predate per-product licenses — launches
|
|
29
33
|
* fall back to the host's global license setting. */
|
|
30
34
|
license?: ProductLicense;
|
package/proxy-middleware.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { logger } from "@norskvideo/ctl-foundation";
|
|
2
2
|
import { buildForwardHeaders, forwardUpstreamResponse, readRequestBody } from "./http-proxy.js";
|
|
3
|
+
import { productBaseUrl } from "./manifest-fetch.js";
|
|
3
4
|
function targetBaseUrl(reg) {
|
|
4
5
|
if (reg.spec.kind === "container") {
|
|
5
|
-
return reg.port !== undefined ?
|
|
6
|
+
return reg.port !== undefined ? productBaseUrl(reg) : null;
|
|
6
7
|
}
|
|
7
8
|
return reg.spec.url.replace(/\/$/, "");
|
|
8
9
|
}
|