@norskvideo/ctl-product-template-schema 0.1.25 → 0.1.27

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/index.d.ts CHANGED
@@ -30,6 +30,9 @@ export declare const ProductTemplateAdvancedSchema: z.ZodObject<{
30
30
  domain: z.ZodOptional<z.ZodObject<{
31
31
  default: z.ZodOptional<z.ZodString>;
32
32
  }, z.core.$strip>>;
33
+ st2110Nics: z.ZodOptional<z.ZodObject<{
34
+ default: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
+ }, z.core.$strip>>;
33
36
  openFileLimit: z.ZodOptional<z.ZodObject<{
34
37
  default: z.ZodOptional<z.ZodNumber>;
35
38
  }, z.core.$strip>>;
@@ -123,6 +126,9 @@ export declare const ProductTemplateRequirementsSchema: z.ZodObject<{
123
126
  }>;
124
127
  value: z.ZodOptional<z.ZodArray<z.ZodString>>;
125
128
  }, z.core.$strip>>;
129
+ st2110Nics: z.ZodOptional<z.ZodObject<{
130
+ count: z.ZodNumber;
131
+ }, z.core.$strip>>;
126
132
  }, z.core.$strip>;
127
133
  export type ProductTemplateRequirements = z.infer<typeof ProductTemplateRequirementsSchema>;
128
134
  export declare const PRODUCT_TEMPLATE_MIGRATION_STRATEGIES: readonly ["overlap", "stopThenStart", "handover"];
@@ -149,9 +155,9 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
149
155
  runtimeScreenLabel: z.ZodOptional<z.ZodString>;
150
156
  runtimeScreenService: z.ZodOptional<z.ZodString>;
151
157
  runtimeScreenReadyWhen: z.ZodOptional<z.ZodEnum<{
158
+ never: "never";
152
159
  running: "running";
153
160
  healthy: "healthy";
154
- never: "never";
155
161
  }>>;
156
162
  runtimeReadyPath: z.ZodOptional<z.ZodString>;
157
163
  }, z.core.$strip>>;
@@ -205,6 +211,9 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
205
211
  domain: z.ZodOptional<z.ZodObject<{
206
212
  default: z.ZodOptional<z.ZodString>;
207
213
  }, z.core.$strip>>;
214
+ st2110Nics: z.ZodOptional<z.ZodObject<{
215
+ default: z.ZodOptional<z.ZodArray<z.ZodString>>;
216
+ }, z.core.$strip>>;
208
217
  openFileLimit: z.ZodOptional<z.ZodObject<{
209
218
  default: z.ZodOptional<z.ZodNumber>;
210
219
  }, z.core.$strip>>;
@@ -274,6 +283,9 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
274
283
  }>;
275
284
  value: z.ZodOptional<z.ZodArray<z.ZodString>>;
276
285
  }, z.core.$strip>>;
286
+ st2110Nics: z.ZodOptional<z.ZodObject<{
287
+ count: z.ZodNumber;
288
+ }, z.core.$strip>>;
277
289
  }, z.core.$strip>>;
278
290
  allocatedPorts: z.ZodOptional<z.ZodArray<z.ZodObject<{
279
291
  param: z.ZodString;
@@ -285,6 +297,25 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
285
297
  preferred: z.ZodNumber;
286
298
  label: z.ZodOptional<z.ZodString>;
287
299
  }, z.core.$strip>>>;
300
+ logs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
301
+ stdout: z.ZodOptional<z.ZodEnum<{
302
+ json: "json";
303
+ text: "text";
304
+ none: "none";
305
+ }>>;
306
+ files: z.ZodOptional<z.ZodArray<z.ZodObject<{
307
+ path: z.ZodString;
308
+ format: z.ZodEnum<{
309
+ json: "json";
310
+ text: "text";
311
+ }>;
312
+ }, z.core.$strict>>>;
313
+ fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
314
+ configFlag: z.ZodString;
315
+ }, z.core.$strict>, z.ZodObject<{
316
+ configPath: z.ZodString;
317
+ }, z.core.$strict>]>>;
318
+ }, z.core.$strict>>>;
288
319
  migration: z.ZodOptional<z.ZodObject<{
289
320
  strategy: z.ZodEnum<{
290
321
  overlap: "overlap";
@@ -359,9 +390,9 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
359
390
  runtimeScreenLabel: z.ZodOptional<z.ZodString>;
360
391
  runtimeScreenService: z.ZodOptional<z.ZodString>;
361
392
  runtimeScreenReadyWhen: z.ZodOptional<z.ZodEnum<{
393
+ never: "never";
362
394
  running: "running";
363
395
  healthy: "healthy";
364
- never: "never";
365
396
  }>>;
366
397
  runtimeReadyPath: z.ZodOptional<z.ZodString>;
367
398
  }, z.core.$strip>>;
@@ -415,6 +446,9 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
415
446
  domain: z.ZodOptional<z.ZodObject<{
416
447
  default: z.ZodOptional<z.ZodString>;
417
448
  }, z.core.$strip>>;
449
+ st2110Nics: z.ZodOptional<z.ZodObject<{
450
+ default: z.ZodOptional<z.ZodArray<z.ZodString>>;
451
+ }, z.core.$strip>>;
418
452
  openFileLimit: z.ZodOptional<z.ZodObject<{
419
453
  default: z.ZodOptional<z.ZodNumber>;
420
454
  }, z.core.$strip>>;
@@ -484,6 +518,9 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
484
518
  }>;
485
519
  value: z.ZodOptional<z.ZodArray<z.ZodString>>;
486
520
  }, z.core.$strip>>;
521
+ st2110Nics: z.ZodOptional<z.ZodObject<{
522
+ count: z.ZodNumber;
523
+ }, z.core.$strip>>;
487
524
  }, z.core.$strip>>;
488
525
  allocatedPorts: z.ZodOptional<z.ZodArray<z.ZodObject<{
489
526
  param: z.ZodString;
@@ -495,6 +532,25 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
495
532
  preferred: z.ZodNumber;
496
533
  label: z.ZodOptional<z.ZodString>;
497
534
  }, z.core.$strip>>>;
535
+ logs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
536
+ stdout: z.ZodOptional<z.ZodEnum<{
537
+ json: "json";
538
+ text: "text";
539
+ none: "none";
540
+ }>>;
541
+ files: z.ZodOptional<z.ZodArray<z.ZodObject<{
542
+ path: z.ZodString;
543
+ format: z.ZodEnum<{
544
+ json: "json";
545
+ text: "text";
546
+ }>;
547
+ }, z.core.$strict>>>;
548
+ fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
549
+ configFlag: z.ZodString;
550
+ }, z.core.$strict>, z.ZodObject<{
551
+ configPath: z.ZodString;
552
+ }, z.core.$strict>]>>;
553
+ }, z.core.$strict>>>;
498
554
  migration: z.ZodOptional<z.ZodObject<{
499
555
  strategy: z.ZodEnum<{
500
556
  overlap: "overlap";
@@ -542,4 +598,4 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
542
598
  }, z.core.$strip>>>;
543
599
  }, z.core.$strip>;
544
600
  export type ProductTemplateMaterials = z.infer<typeof ProductTemplateMaterialsSchema>;
545
- export {};
601
+ export { composeServiceNamesOf, logsDeclarationProblems, NORSK_MEDIA_SERVICE_LOGS, type ProductTemplateLogs, ProductTemplateLogsSchema, productTemplateLogsProblems, type ServiceLogs, ServiceLogsSchema, STANDARD_LOGS, STUDIO_SERVICE_LOGS, servicesWithoutLogs, } from "./logs.js";
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ProductTemplateLogsSchema } from "./logs.js";
2
3
  // ProductTemplate format v1. A product template is a tar file at the wire; the schemas
3
4
  // below describe its parts before packing and how the runner validates
4
5
  // the unpacked tar.
@@ -134,6 +135,10 @@ export const ProductTemplateAdvancedSchema = z
134
135
  // without the operator naming it; a launch can override, or decline with
135
136
  // "" to run isolated.
136
137
  domain: z.object({ default: z.string().optional() }).optional(),
138
+ // ST 2110 NICs (by interface name) an instance claims for MTL. A product
139
+ // that knows its host (a customer turnkey) can name them as its default;
140
+ // the launch overrides, or declines with [].
141
+ st2110Nics: z.object({ default: z.array(z.string()).optional() }).optional(),
137
142
  // Open file descriptors per container. The runner already states a ceiling
138
143
  // for every instance (unset, containers inherit the host daemon's limit,
139
144
  // which is how a media graph at ~1000 handles dies of EMFILE on some hosts
@@ -267,6 +272,9 @@ export const ProductTemplateRequirementsSchema = z
267
272
  hostPorts: requirementDimension(z.array(z.string().regex(/^\d+\/(tcp|udp)$/i, "canonical PORT/proto token, e.g. 5001/udp")).min(1))
268
273
  .optional()
269
274
  .meta({ description: "Fixed host sockets the job binds (exclusive)." }),
275
+ st2110Nics: z.object({ count: z.number().int().positive() }).optional().meta({
276
+ description: "How many ST 2110 NICs an instance must claim. For a product that cannot know which NICs a host has: the operator picks them at launch (advanced.st2110Nics names a default).",
277
+ }),
270
278
  })
271
279
  .meta({ id: "ProductTemplateRequirements", outputId: "ProductTemplateRequirements" });
272
280
  // How a running instance of the product may be moved between nodes
@@ -328,6 +336,7 @@ export const ProductTemplateManifestSchema = z
328
336
  allocatedPorts: z.array(ProductTemplateAllocatedPortSchema).optional().meta({
329
337
  description: "Host-port bindings whose value rides a product-template parameter. Opt-in per launch via the param value: absent = closed, 'auto' = allocate a free port from `preferred`, number = use it. When opened the port is published as a managed host binding and surfaced to the containers via compose interpolation. Alongside the per-param scalars the runner sets NORSK_ALLOCATED_PORTS: a JSON object keyed by param, each value {port, protocol, service?}, describing every opened port ('{}' when none opted in; unset when nothing is declared) — so a product needing the allocation as data maps one var instead of folding scalars into JSON via interpolation.",
330
338
  }),
339
+ logs: ProductTemplateLogsSchema.optional(),
331
340
  migration: ProductTemplateMigrationSchema.optional().meta({
332
341
  description: "How a running instance may be moved between nodes. `strategy` is the strongest promise the product keeps: `overlap` (target started, then source stopped — both may produce briefly), `stopThenStart` (the default when absent — target prepared but not started, source stopped, target started), or `handover` (the job speaks the worker's migration socket and hands over at an exact position). A migrate request may down-grade to a safer strategy but never up-grade past the declaration. Only the manager reads this; the worker never does.",
333
342
  }),
@@ -435,3 +444,4 @@ export const ProductTemplateMaterialsSchema = z
435
444
  }),
436
445
  })
437
446
  .meta({ id: "ProductTemplateMaterials" });
447
+ export { composeServiceNamesOf, logsDeclarationProblems, NORSK_MEDIA_SERVICE_LOGS, ProductTemplateLogsSchema, productTemplateLogsProblems, ServiceLogsSchema, STANDARD_LOGS, STUDIO_SERVICE_LOGS, servicesWithoutLogs, } from "./logs.js";
package/logs.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+ export declare const ServiceLogFileSchema: z.ZodObject<{
3
+ path: z.ZodString;
4
+ format: z.ZodEnum<{
5
+ json: "json";
6
+ text: "text";
7
+ }>;
8
+ }, z.core.$strict>;
9
+ export declare const ServiceFluentBitSchema: z.ZodUnion<readonly [z.ZodObject<{
10
+ configFlag: z.ZodString;
11
+ }, z.core.$strict>, z.ZodObject<{
12
+ configPath: z.ZodString;
13
+ }, z.core.$strict>]>;
14
+ export declare const ServiceLogsSchema: z.ZodObject<{
15
+ stdout: z.ZodOptional<z.ZodEnum<{
16
+ json: "json";
17
+ text: "text";
18
+ none: "none";
19
+ }>>;
20
+ files: z.ZodOptional<z.ZodArray<z.ZodObject<{
21
+ path: z.ZodString;
22
+ format: z.ZodEnum<{
23
+ json: "json";
24
+ text: "text";
25
+ }>;
26
+ }, z.core.$strict>>>;
27
+ fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
28
+ configFlag: z.ZodString;
29
+ }, z.core.$strict>, z.ZodObject<{
30
+ configPath: z.ZodString;
31
+ }, z.core.$strict>]>>;
32
+ }, z.core.$strict>;
33
+ export type ServiceLogs = z.infer<typeof ServiceLogsSchema>;
34
+ export declare const ProductTemplateLogsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
35
+ stdout: z.ZodOptional<z.ZodEnum<{
36
+ json: "json";
37
+ text: "text";
38
+ none: "none";
39
+ }>>;
40
+ files: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ path: z.ZodString;
42
+ format: z.ZodEnum<{
43
+ json: "json";
44
+ text: "text";
45
+ }>;
46
+ }, z.core.$strict>>>;
47
+ fluentBit: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
48
+ configFlag: z.ZodString;
49
+ }, z.core.$strict>, z.ZodObject<{
50
+ configPath: z.ZodString;
51
+ }, z.core.$strict>]>>;
52
+ }, z.core.$strict>>;
53
+ export type ProductTemplateLogs = z.infer<typeof ProductTemplateLogsSchema>;
54
+ /** The service names a compose file defines (empty if it is not a compose
55
+ * document with `services`). */
56
+ export declare function composeServiceNamesOf(composeYaml: string): string[];
57
+ /** Problems that make a `logs` block unusable: a service compose.yml does not
58
+ * define, or two files of one service in the same directory with different
59
+ * formats (they are mounted together, so they would be read one way). */
60
+ export declare function logsDeclarationProblems(logs: ProductTemplateLogs | undefined, composeYaml: string): string[];
61
+ /** Compose services with no `logs` entry — what the dev-kit refuses, so a new
62
+ * sidecar cannot ship nothing (or garbage) without anyone deciding so. */
63
+ export declare function servicesWithoutLogs(logs: ProductTemplateLogs | undefined, composeYaml: string): string[];
64
+ /** Everything wrong with a product template's logging, for a product's own
65
+ * tests (the dev-kit's drift gate requires one to call this): an unusable
66
+ * `logs` block, and every compose service without an entry. Empty = fine. */
67
+ export declare function productTemplateLogsProblems(materials: {
68
+ manifest: {
69
+ logs?: ProductTemplateLogs;
70
+ };
71
+ composeYaml: string;
72
+ }): string[];
73
+ /** Norsk Studio (the `studio` service every Studio-based product runs): its
74
+ * winston logger writes one JSON object per line to debug.log under the
75
+ * config's server.logs — /usr/src/app/logs in the image — while its stdout is
76
+ * a coloured, multi-line copy for humans, so it is not shipped. (error.log
77
+ * beside it repeats debug.log's errors, so it is not declared.) */
78
+ export declare const STUDIO_SERVICE_LOGS: ServiceLogs;
79
+ /** The Norsk engine (the `media` service): it runs its own fluent-bit, which
80
+ * already collects its logs and stdout/stderr (tags media-erlang,
81
+ * media-rust, media-stderr), and @INCLUDEs the outputs file named by
82
+ * --log-config-file. */
83
+ export declare const NORSK_MEDIA_SERVICE_LOGS: ServiceLogs;
84
+ /** The `logs:` block for the reserved studio + media pair. A product with
85
+ * more services spreads this and declares its own beside it. */
86
+ export declare const STANDARD_LOGS: ProductTemplateLogs;
package/logs.js ADDED
@@ -0,0 +1,116 @@
1
+ // How each of a product's compose services logs, so the node's log agent can
2
+ // ship it (norsk-mgr docs/internal/design/product-logs.md). The worker reads
3
+ // this; the product never names a destination.
4
+ import { parse as yamlParse } from "yaml";
5
+ import { z } from "zod";
6
+ const absolutePath = z.string().regex(/^\/[^\0]*[^/]$/, "an absolute path to a file (not a directory, no trailing /)");
7
+ export const ServiceLogFileSchema = z
8
+ .strictObject({
9
+ path: absolutePath.meta({
10
+ description: "The log file inside the container. Its directory is mounted onto the host.",
11
+ }),
12
+ format: z
13
+ .enum(["json", "text"])
14
+ .meta({ description: "json = one JSON object per line; text = one event per line." }),
15
+ })
16
+ .meta({ id: "ServiceLogFile" });
17
+ export const ServiceFluentBitSchema = z
18
+ .union([
19
+ z.strictObject({
20
+ configFlag: z
21
+ .string()
22
+ .regex(/^--?[A-Za-z0-9][A-Za-z0-9-]*$/, "a command-line flag, e.g. --log-config-file")
23
+ .meta({ description: "Appended to the service's command, followed by the mounted outputs file's path." }),
24
+ }),
25
+ z.strictObject({
26
+ configPath: absolutePath.meta({
27
+ description: "Where the outputs file is mounted; the image reads it from there.",
28
+ }),
29
+ }),
30
+ ])
31
+ .meta({
32
+ id: "ServiceFluentBit",
33
+ description: "The service runs its own fluent-bit and reads an [OUTPUT] file: the worker hands it the node's (forward to the log agent, or the operator's config without one).",
34
+ });
35
+ export const ServiceLogsSchema = z
36
+ .strictObject({
37
+ stdout: z.enum(["text", "json", "none"]).optional().meta({
38
+ description: "The service's stdout/stderr: text (default; one event per line), json (one JSON object per line), none (not shipped — e.g. a human-readable copy of a log declared under files).",
39
+ }),
40
+ files: z.array(ServiceLogFileSchema).optional(),
41
+ fluentBit: ServiceFluentBitSchema.optional(),
42
+ })
43
+ .meta({ id: "ServiceLogs" });
44
+ export const ProductTemplateLogsSchema = z.record(z.string().min(1), ServiceLogsSchema).meta({
45
+ id: "ProductTemplateLogs",
46
+ description: "Per compose service: how it logs, so the node's log agent ships it. Absent = the runner's defaults (the `media` service gets --log-config-file, Studio's log directory is mounted). The dev-kit requires every service to have an entry.",
47
+ });
48
+ /** The service names a compose file defines (empty if it is not a compose
49
+ * document with `services`). */
50
+ export function composeServiceNamesOf(composeYaml) {
51
+ const doc = yamlParse(composeYaml);
52
+ return doc?.services && typeof doc.services === "object" ? Object.keys(doc.services) : [];
53
+ }
54
+ /** Problems that make a `logs` block unusable: a service compose.yml does not
55
+ * define, or two files of one service in the same directory with different
56
+ * formats (they are mounted together, so they would be read one way). */
57
+ export function logsDeclarationProblems(logs, composeYaml) {
58
+ if (!logs)
59
+ return [];
60
+ const services = new Set(composeServiceNamesOf(composeYaml));
61
+ const problems = [];
62
+ for (const [service, decl] of Object.entries(logs)) {
63
+ if (!services.has(service))
64
+ problems.push(`logs.${service}: compose.yml has no service '${service}'`);
65
+ const formatByDir = new Map();
66
+ for (const f of decl.files ?? []) {
67
+ const dir = f.path.slice(0, f.path.lastIndexOf("/")) || "/";
68
+ if (dir === "/")
69
+ problems.push(`logs.${service}: ${f.path} sits in / — a log file needs a directory of its own`);
70
+ const seen = formatByDir.get(dir);
71
+ if (seen !== undefined && seen !== f.format) {
72
+ problems.push(`logs.${service}: files in ${dir} are declared both ${seen} and ${f.format}`);
73
+ }
74
+ formatByDir.set(dir, f.format);
75
+ }
76
+ }
77
+ return problems;
78
+ }
79
+ /** Compose services with no `logs` entry — what the dev-kit refuses, so a new
80
+ * sidecar cannot ship nothing (or garbage) without anyone deciding so. */
81
+ export function servicesWithoutLogs(logs, composeYaml) {
82
+ const declared = new Set(Object.keys(logs ?? {}));
83
+ return composeServiceNamesOf(composeYaml).filter((s) => !declared.has(s));
84
+ }
85
+ /** Everything wrong with a product template's logging, for a product's own
86
+ * tests (the dev-kit's drift gate requires one to call this): an unusable
87
+ * `logs` block, and every compose service without an entry. Empty = fine. */
88
+ export function productTemplateLogsProblems(materials) {
89
+ return [
90
+ ...logsDeclarationProblems(materials.manifest.logs, materials.composeYaml),
91
+ ...servicesWithoutLogs(materials.manifest.logs, materials.composeYaml).map((s) => `compose service '${s}' has no logs entry — say how it logs (stdout/files/fluentBit), or \`${s}: { stdout: none }\``),
92
+ ];
93
+ }
94
+ /** Norsk Studio (the `studio` service every Studio-based product runs): its
95
+ * winston logger writes one JSON object per line to debug.log under the
96
+ * config's server.logs — /usr/src/app/logs in the image — while its stdout is
97
+ * a coloured, multi-line copy for humans, so it is not shipped. (error.log
98
+ * beside it repeats debug.log's errors, so it is not declared.) */
99
+ export const STUDIO_SERVICE_LOGS = {
100
+ stdout: "none",
101
+ files: [{ path: "/usr/src/app/logs/debug.log", format: "json" }],
102
+ };
103
+ /** The Norsk engine (the `media` service): it runs its own fluent-bit, which
104
+ * already collects its logs and stdout/stderr (tags media-erlang,
105
+ * media-rust, media-stderr), and @INCLUDEs the outputs file named by
106
+ * --log-config-file. */
107
+ export const NORSK_MEDIA_SERVICE_LOGS = {
108
+ stdout: "none",
109
+ fluentBit: { configFlag: "--log-config-file" },
110
+ };
111
+ /** The `logs:` block for the reserved studio + media pair. A product with
112
+ * more services spreads this and declares its own beside it. */
113
+ export const STANDARD_LOGS = {
114
+ studio: STUDIO_SERVICE_LOGS,
115
+ media: NORSK_MEDIA_SERVICE_LOGS,
116
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-product-template-schema",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {