@norskvideo/ctl-sdk 0.1.18 → 0.1.19
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 +13 -0
- package/package.json +1 -1
- package/product-error.d.ts +2 -2
- package/product-service.d.ts +9 -0
- package/product-service.js +23 -3
package/docker-runner.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export declare function productRunArgs(image: string, hostPort: number): string[
|
|
|
11
11
|
* `docker ps` shows `norsk-product-studio` instead of a random docker alias.
|
|
12
12
|
* Mirrors the singleton naming of norsk-proxy / norsk-ctl-cpu-monitor. */
|
|
13
13
|
export declare function productContainerName(productName: string): string;
|
|
14
|
+
/** An `@sha256:...` (or any `@algo:hex`) reference names one immutable image,
|
|
15
|
+
* so there is nothing to refresh; a tag can move under a registration. */
|
|
16
|
+
export declare function isDigestRef(image: string): boolean;
|
|
17
|
+
export declare function dockerPull(image: string): Promise<void>;
|
|
14
18
|
export declare function dockerRun(image: string, hostPort: number): Promise<string>;
|
|
15
19
|
export declare function dockerRm(containerId: string): Promise<void>;
|
|
16
20
|
/** Best-effort rename of a running container. The product is tracked by
|
package/docker-runner.js
CHANGED
|
@@ -30,6 +30,19 @@ export function productContainerName(productName) {
|
|
|
30
30
|
.replace(/^-+|-+$/g, "");
|
|
31
31
|
return `norsk-product-${slug || "unnamed"}`;
|
|
32
32
|
}
|
|
33
|
+
/** An `@sha256:...` (or any `@algo:hex`) reference names one immutable image,
|
|
34
|
+
* so there is nothing to refresh; a tag can move under a registration. */
|
|
35
|
+
export function isDigestRef(image) {
|
|
36
|
+
return /@[a-z0-9]+:[0-9a-f]+$/i.test(image);
|
|
37
|
+
}
|
|
38
|
+
export async function dockerPull(image) {
|
|
39
|
+
const proc = Bun.spawn(["docker", "pull", image], { stdout: "pipe", stderr: "pipe" });
|
|
40
|
+
const exitCode = await proc.exited;
|
|
41
|
+
const stderr = (await new Response(proc.stderr).text()).trim();
|
|
42
|
+
if (exitCode !== 0) {
|
|
43
|
+
throw new ProductError("DOCKER_PULL_FAILED", `docker pull failed (exit ${exitCode}): ${stderr || "no stderr"}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
33
46
|
export async function dockerRun(image, hostPort) {
|
|
34
47
|
const proc = Bun.spawn(["docker", ...productRunArgs(image, hostPort)], { stdout: "pipe", stderr: "pipe" });
|
|
35
48
|
const exitCode = await proc.exited;
|
package/package.json
CHANGED
package/product-error.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare class ProductError extends Error {
|
|
2
|
-
code: "DEV_URL_INVALID" | "DEV_URL_NOT_LOCALHOST" | "PORT_EXHAUSTED" | "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" | "PRODUCT_TEMPLATE_FETCH_FAILED";
|
|
3
|
-
constructor(code: "DEV_URL_INVALID" | "DEV_URL_NOT_LOCALHOST" | "PORT_EXHAUSTED" | "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" | "PRODUCT_TEMPLATE_FETCH_FAILED", message: string);
|
|
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" | "PRODUCT_TEMPLATE_FETCH_FAILED";
|
|
3
|
+
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" | "PRODUCT_TEMPLATE_FETCH_FAILED", message: string);
|
|
4
4
|
}
|
package/product-service.d.ts
CHANGED
|
@@ -55,11 +55,15 @@ export type ImportProductTemplateBytesFn = (opts: {
|
|
|
55
55
|
* module functions: calling docker-runner directly re-creates the bypass that
|
|
56
56
|
* made an injected fake silently inert on add/remove. */
|
|
57
57
|
export interface ProductContainerOps {
|
|
58
|
+
pull(image: string): Promise<void>;
|
|
58
59
|
run(image: string, hostPort: number): Promise<string>;
|
|
59
60
|
remove(containerId: string): Promise<void>;
|
|
60
61
|
rename(containerId: string, name: string): Promise<void>;
|
|
61
62
|
waitForReady(baseUrl: string): Promise<void>;
|
|
62
63
|
}
|
|
64
|
+
/** The real docker-runner wiring. Exported so a host can override one op
|
|
65
|
+
* (typically `pull`, onto its own docker adapter) and keep the rest. */
|
|
66
|
+
export declare const defaultProductContainerOps: ProductContainerOps;
|
|
63
67
|
export interface ProductServiceOptions {
|
|
64
68
|
store: ProductStore;
|
|
65
69
|
allocatePort: AllocatePortFn;
|
|
@@ -102,6 +106,11 @@ export declare class ProductService {
|
|
|
102
106
|
private readonly containerOps;
|
|
103
107
|
constructor(opts: ProductServiceOptions);
|
|
104
108
|
list(): ProductRegistration[];
|
|
109
|
+
/** Docker's default `--pull=missing` keeps whatever a moving tag resolved to
|
|
110
|
+
* last time, so a rebuilt `:latest`/`:dev` never reached a new registration.
|
|
111
|
+
* Best-effort: a locally built image is in no registry, and the run that
|
|
112
|
+
* follows must still get its turn. */
|
|
113
|
+
private refreshImage;
|
|
105
114
|
/** Whether the product is actually up. Container-mode: we started it, so a
|
|
106
115
|
* tracked containerId means running. Dev-mode: externally owned, so probe
|
|
107
116
|
* the dev URL — registered no longer implies running. */
|
package/product-service.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { dockerRename, dockerRm, dockerRun, productContainerName } from "./docker-runner.js";
|
|
4
|
+
import { 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
7
|
import { fetchManifest, isDevUrlAlive, probeConfigScreen, specBaseUrl, waitForReady } from "./manifest-fetch.js";
|
|
@@ -24,7 +24,10 @@ async function fetchProductTemplateBytes(baseUrl, url) {
|
|
|
24
24
|
}
|
|
25
25
|
return new Uint8Array(await response.arrayBuffer());
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** The real docker-runner wiring. Exported so a host can override one op
|
|
28
|
+
* (typically `pull`, onto its own docker adapter) and keep the rest. */
|
|
29
|
+
export const defaultProductContainerOps = {
|
|
30
|
+
pull: dockerPull,
|
|
28
31
|
run: dockerRun,
|
|
29
32
|
remove: dockerRm,
|
|
30
33
|
rename: dockerRename,
|
|
@@ -58,11 +61,27 @@ export class ProductService {
|
|
|
58
61
|
this.importProductTemplateBytes = opts.importProductTemplateBytes;
|
|
59
62
|
this.stageLicense = opts.stageLicense;
|
|
60
63
|
this.isDevUrlAlive = opts.isDevUrlAlive ?? isDevUrlAlive;
|
|
61
|
-
this.containerOps = opts.containerOps ??
|
|
64
|
+
this.containerOps = opts.containerOps ?? defaultProductContainerOps;
|
|
62
65
|
}
|
|
63
66
|
list() {
|
|
64
67
|
return this.store.read();
|
|
65
68
|
}
|
|
69
|
+
/** Docker's default `--pull=missing` keeps whatever a moving tag resolved to
|
|
70
|
+
* last time, so a rebuilt `:latest`/`:dev` never reached a new registration.
|
|
71
|
+
* Best-effort: a locally built image is in no registry, and the run that
|
|
72
|
+
* follows must still get its turn. */
|
|
73
|
+
async refreshImage(image) {
|
|
74
|
+
if (isDigestRef(image))
|
|
75
|
+
return;
|
|
76
|
+
logger.info(`Pulling product image: ${image}`);
|
|
77
|
+
try {
|
|
78
|
+
await this.containerOps.pull(image);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
82
|
+
logger.warn(`Pull of ${image} did not complete (${msg}); continuing with the local image if present`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
66
85
|
/** Whether the product is actually up. Container-mode: we started it, so a
|
|
67
86
|
* tracked containerId means running. Dev-mode: externally owned, so probe
|
|
68
87
|
* the dev URL — registered no longer implies running. */
|
|
@@ -93,6 +112,7 @@ export class ProductService {
|
|
|
93
112
|
throw new ProductError("PORT_EXHAUSTED", `no free port available for new product`);
|
|
94
113
|
}
|
|
95
114
|
port = allocated;
|
|
115
|
+
await this.refreshImage(spec.image);
|
|
96
116
|
logger.info(`Starting product container: ${spec.image} on host port ${port}`);
|
|
97
117
|
containerId = await this.containerOps.run(spec.image, port);
|
|
98
118
|
baseUrl = specBaseUrl(spec, port);
|