@norskvideo/ctl-sdk 0.1.17 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-sdk",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -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
  }
@@ -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. */
@@ -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
- const defaultContainerOps = {
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 ?? defaultContainerOps;
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);
@@ -49,6 +49,10 @@ export function parseProductTemplatesFile(raw, path) {
49
49
  rec.sha256 = entry.sha256;
50
50
  if (isRecord(entry.launchDefaults))
51
51
  rec.launchDefaults = entry.launchDefaults;
52
+ if (isRecord(entry.parameterDefaults) &&
53
+ Object.values(entry.parameterDefaults).every((v) => typeof v === "string")) {
54
+ rec.parameterDefaults = entry.parameterDefaults;
55
+ }
52
56
  if (Array.isArray(entry.suggestedSecurityGroups) &&
53
57
  entry.suggestedSecurityGroups.every((l) => typeof l === "string")) {
54
58
  rec.suggestedSecurityGroups = entry.suggestedSecurityGroups;
@@ -49,6 +49,14 @@ export interface ProductTemplateRecord {
49
49
  * authoritatively by the worker at launch. Rides launches as
50
50
  * template_launch_defaults_json. */
51
51
  launchDefaults?: Record<string, unknown>;
52
+ /** Template-author parameter tier (env-var authoring): name → default
53
+ * value. A name the product declared overrides the manifest default;
54
+ * an undeclared name ADDS a template-defined variable. Fused into every
55
+ * launch's parameter map beneath the operator's instance values —
56
+ * instance wins, template default wins over manifest default. Names
57
+ * must be env-var shaped and never `NORSK_`-prefixed (that namespace is
58
+ * infra-injected: NORSK_RUNTIME_ROLE, NORSK_MATCHED_CAPABILITIES, …). */
59
+ parameterDefaults?: Record<string, string>;
52
60
  /** Template-author security-group suggestions, by discovery-tag LABEL
53
61
  * (portable across deployments; ids are not). The launch UI pre-ticks
54
62
  * discovered groups whose label matches — a suggestion the operator