@norskvideo/ctl-sdk 0.1.19 → 0.1.20
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-service.d.ts +36 -1
- package/product-service.js +61 -6
package/package.json
CHANGED
package/product-service.d.ts
CHANGED
|
@@ -11,6 +11,22 @@ export interface AddProductResult {
|
|
|
11
11
|
registration: ProductRegistration;
|
|
12
12
|
warnings: string[];
|
|
13
13
|
}
|
|
14
|
+
/** Result of `reload`. `containerRestarted` is false for dev products, which
|
|
15
|
+
* are externally owned. `productTemplates` reports the manifest-declared
|
|
16
|
+
* default templates handed to the host's refresh callback: `skipped` carries
|
|
17
|
+
* the host's reason (typically "in use"), and every skip is also a warning. */
|
|
18
|
+
export interface ReloadProductResult {
|
|
19
|
+
registration: ProductRegistration;
|
|
20
|
+
warnings: string[];
|
|
21
|
+
containerRestarted: boolean;
|
|
22
|
+
productTemplates: {
|
|
23
|
+
refreshed: string[];
|
|
24
|
+
skipped: Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
14
30
|
export interface AddProductOpts {
|
|
15
31
|
/** Per-product license stored on the registration record (#313). Callers
|
|
16
32
|
* typically seed this from their global license setting when the operator
|
|
@@ -49,6 +65,13 @@ export type ImportProductTemplateBytesFn = (opts: {
|
|
|
49
65
|
kind: "product-default";
|
|
50
66
|
}>;
|
|
51
67
|
}) => Promise<void>;
|
|
68
|
+
/** `reload`'s counterpart to ImportProductTemplateBytesFn: re-render the
|
|
69
|
+
* stored snapshot of a product-default template from fresh bytes, or store
|
|
70
|
+
* it if the product only started declaring it. The host owns the refusal
|
|
71
|
+
* rules (a template an instance is using, one that is not product-default);
|
|
72
|
+
* it throws, and the SDK reports the message as a skip rather than failing
|
|
73
|
+
* the reload. */
|
|
74
|
+
export type RefreshProductTemplateBytesFn = ImportProductTemplateBytesFn;
|
|
52
75
|
/** Every container operation this service performs, behind an interface so
|
|
53
76
|
* tests can drive them without shelling out to Docker. Defaults wire straight
|
|
54
77
|
* to docker-runner + the readiness probe. Reach for these rather than the
|
|
@@ -71,6 +94,9 @@ export interface ProductServiceOptions {
|
|
|
71
94
|
* registration time. Omit on hosts that don't store product templates
|
|
72
95
|
* (defaultProductTemplates is then silently skipped). */
|
|
73
96
|
importProductTemplateBytes?: ImportProductTemplateBytesFn;
|
|
97
|
+
/** Optional: called once per `manifest.defaultProductTemplates` entry by
|
|
98
|
+
* `reload`. Omit on hosts that don't store product templates. */
|
|
99
|
+
refreshProductTemplateBytes?: RefreshProductTemplateBytesFn;
|
|
74
100
|
/** Optional: copy a byol licence somewhere the host owns, so the path
|
|
75
101
|
* recorded in the registry outlives whatever the operator passed in.
|
|
76
102
|
* Omit on hosts that don't stage (the operator's path is then recorded). */
|
|
@@ -101,6 +127,7 @@ export declare class ProductService {
|
|
|
101
127
|
private readonly store;
|
|
102
128
|
private readonly allocatePort;
|
|
103
129
|
private readonly importProductTemplateBytes?;
|
|
130
|
+
private readonly refreshProductTemplateBytes?;
|
|
104
131
|
private readonly stageLicense?;
|
|
105
132
|
private readonly isDevUrlAlive;
|
|
106
133
|
private readonly containerOps;
|
|
@@ -119,7 +146,15 @@ export declare class ProductService {
|
|
|
119
146
|
private addSerialised;
|
|
120
147
|
remove(name: string): Promise<void>;
|
|
121
148
|
private removeSerialised;
|
|
122
|
-
|
|
149
|
+
/** Bring a registration up to date with what its image now serves. For a
|
|
150
|
+
* container product: pull the tag (digests cannot move), re-run the
|
|
151
|
+
* control plane on its recorded port, re-read the manifest, then hand every
|
|
152
|
+
* manifest-declared default template to the host to re-render. Dev products
|
|
153
|
+
* are externally owned, so only the manifest is re-read. This is the verb
|
|
154
|
+
* the per-product "remove template, remove product, add again" scripts
|
|
155
|
+
* stood in for. Like restart, the new container id is persisted before the
|
|
156
|
+
* readiness wait so a timed-out reload leaves a tracked container. */
|
|
157
|
+
reload(name: string): Promise<ReloadProductResult>;
|
|
123
158
|
private reloadSerialised;
|
|
124
159
|
/** Stop every running container-kind product and clear its tracked
|
|
125
160
|
* containerId (model B: the daemon owns container lifecycle, so it reaps
|
package/product-service.js
CHANGED
|
@@ -52,6 +52,7 @@ export class ProductService {
|
|
|
52
52
|
store;
|
|
53
53
|
allocatePort;
|
|
54
54
|
importProductTemplateBytes;
|
|
55
|
+
refreshProductTemplateBytes;
|
|
55
56
|
stageLicense;
|
|
56
57
|
isDevUrlAlive;
|
|
57
58
|
containerOps;
|
|
@@ -59,6 +60,7 @@ export class ProductService {
|
|
|
59
60
|
this.store = opts.store;
|
|
60
61
|
this.allocatePort = opts.allocatePort;
|
|
61
62
|
this.importProductTemplateBytes = opts.importProductTemplateBytes;
|
|
63
|
+
this.refreshProductTemplateBytes = opts.refreshProductTemplateBytes;
|
|
62
64
|
this.stageLicense = opts.stageLicense;
|
|
63
65
|
this.isDevUrlAlive = opts.isDevUrlAlive ?? isDevUrlAlive;
|
|
64
66
|
this.containerOps = opts.containerOps ?? defaultProductContainerOps;
|
|
@@ -272,23 +274,76 @@ export class ProductService {
|
|
|
272
274
|
await this.store.update((products) => products.filter((p) => p.name !== name));
|
|
273
275
|
logger.info(`Product '${name}' removed`);
|
|
274
276
|
}
|
|
277
|
+
/** Bring a registration up to date with what its image now serves. For a
|
|
278
|
+
* container product: pull the tag (digests cannot move), re-run the
|
|
279
|
+
* control plane on its recorded port, re-read the manifest, then hand every
|
|
280
|
+
* manifest-declared default template to the host to re-render. Dev products
|
|
281
|
+
* are externally owned, so only the manifest is re-read. This is the verb
|
|
282
|
+
* the per-product "remove template, remove product, add again" scripts
|
|
283
|
+
* stood in for. Like restart, the new container id is persisted before the
|
|
284
|
+
* readiness wait so a timed-out reload leaves a tracked container. */
|
|
275
285
|
async reload(name) {
|
|
276
286
|
return this.mutations.run(() => this.reloadSerialised(name));
|
|
277
287
|
}
|
|
278
288
|
async reloadSerialised(name) {
|
|
279
|
-
const
|
|
280
|
-
const target = existing.find((p) => p.name === name);
|
|
289
|
+
const target = this.store.read().find((p) => p.name === name);
|
|
281
290
|
if (!target)
|
|
282
291
|
throw new ProductError("NOT_FOUND", `product '${name}' not registered`);
|
|
292
|
+
let containerRestarted = false;
|
|
293
|
+
if (target.spec.kind === "container") {
|
|
294
|
+
if (target.port === undefined) {
|
|
295
|
+
throw new ProductError("NOT_RESTARTABLE", `product '${name}' has no recorded port — cannot reload`);
|
|
296
|
+
}
|
|
297
|
+
await this.refreshImage(target.spec.image);
|
|
298
|
+
if (target.containerId) {
|
|
299
|
+
try {
|
|
300
|
+
await this.containerOps.remove(target.containerId);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Already gone (crashed / --rm reaped) — nothing to stop.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const containerId = await this.containerOps.run(target.spec.image, target.port);
|
|
307
|
+
await this.store.update((products) => products.map((p) => (p.name === name ? withContainerId(p, containerId) : p)));
|
|
308
|
+
await this.containerOps.waitForReady(specBaseUrl(target.spec, target.port));
|
|
309
|
+
await this.containerOps.rename(containerId, productContainerName(name));
|
|
310
|
+
containerRestarted = true;
|
|
311
|
+
}
|
|
283
312
|
const baseUrl = specBaseUrl(target.spec, target.port);
|
|
284
313
|
const manifest = await fetchManifest(baseUrl);
|
|
285
314
|
if (manifest.name !== target.name) {
|
|
286
315
|
throw new ProductError("NAME_CONFLICT", `manifest now reports name '${manifest.name}', was '${target.name}' — use remove + add`);
|
|
287
316
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
logger.info(`Product '${name}'
|
|
291
|
-
|
|
317
|
+
const updated = await this.store.update((products) => products.map((p) => (p.name === name ? { ...p, manifest } : p)));
|
|
318
|
+
const registration = updated.find((p) => p.name === name);
|
|
319
|
+
logger.info(`Product '${name}' reloaded${containerRestarted ? " (container re-run)" : ""}`);
|
|
320
|
+
// Same downgrade as add's default-template import: the product itself is
|
|
321
|
+
// healthy, so a template the host refuses (in use, foreign) or a fetch
|
|
322
|
+
// that fails is a warning the operator acts on, not a failed reload.
|
|
323
|
+
const warnings = [];
|
|
324
|
+
const productTemplates = { refreshed: [], skipped: [] };
|
|
325
|
+
if (this.refreshProductTemplateBytes) {
|
|
326
|
+
for (const entry of manifest.defaultProductTemplates) {
|
|
327
|
+
try {
|
|
328
|
+
const bytes = await fetchProductTemplateBytes(baseUrl, entry.url);
|
|
329
|
+
await this.refreshProductTemplateBytes({
|
|
330
|
+
name: entry.name,
|
|
331
|
+
bytes,
|
|
332
|
+
source: { kind: "product-default", productName: manifest.name, url: entry.url },
|
|
333
|
+
});
|
|
334
|
+
productTemplates.refreshed.push(entry.name);
|
|
335
|
+
logger.info(`Product '${name}': refreshed default product template '${entry.name}' from ${entry.url}`);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
339
|
+
productTemplates.skipped.push({ name: entry.name, reason });
|
|
340
|
+
warnings.push(`default product template '${entry.name}' not refreshed: ${reason}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const w of warnings)
|
|
345
|
+
logger.warn(`[product:${name}] ${w}`);
|
|
346
|
+
return { registration, warnings, containerRestarted, productTemplates };
|
|
292
347
|
}
|
|
293
348
|
/** Stop every running container-kind product and clear its tracked
|
|
294
349
|
* containerId (model B: the daemon owns container lifecycle, so it reaps
|