@norskvideo/ctl-product-template-schema 0.1.26 → 0.1.28

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
@@ -155,9 +155,9 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
155
155
  runtimeScreenLabel: z.ZodOptional<z.ZodString>;
156
156
  runtimeScreenService: z.ZodOptional<z.ZodString>;
157
157
  runtimeScreenReadyWhen: z.ZodOptional<z.ZodEnum<{
158
+ never: "never";
158
159
  running: "running";
159
160
  healthy: "healthy";
160
- never: "never";
161
161
  }>>;
162
162
  runtimeReadyPath: z.ZodOptional<z.ZodString>;
163
163
  }, z.core.$strip>>;
@@ -297,6 +297,25 @@ export declare const ProductTemplateManifestSchema: z.ZodObject<{
297
297
  preferred: z.ZodNumber;
298
298
  label: z.ZodOptional<z.ZodString>;
299
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>>>;
300
319
  migration: z.ZodOptional<z.ZodObject<{
301
320
  strategy: z.ZodEnum<{
302
321
  overlap: "overlap";
@@ -371,9 +390,9 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
371
390
  runtimeScreenLabel: z.ZodOptional<z.ZodString>;
372
391
  runtimeScreenService: z.ZodOptional<z.ZodString>;
373
392
  runtimeScreenReadyWhen: z.ZodOptional<z.ZodEnum<{
393
+ never: "never";
374
394
  running: "running";
375
395
  healthy: "healthy";
376
- never: "never";
377
396
  }>>;
378
397
  runtimeReadyPath: z.ZodOptional<z.ZodString>;
379
398
  }, z.core.$strip>>;
@@ -513,6 +532,25 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
513
532
  preferred: z.ZodNumber;
514
533
  label: z.ZodOptional<z.ZodString>;
515
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>>>;
516
554
  migration: z.ZodOptional<z.ZodObject<{
517
555
  strategy: z.ZodEnum<{
518
556
  overlap: "overlap";
@@ -560,4 +598,4 @@ export declare const ProductTemplateMaterialsSchema: z.ZodObject<{
560
598
  }, z.core.$strip>>>;
561
599
  }, z.core.$strip>;
562
600
  export type ProductTemplateMaterials = z.infer<typeof ProductTemplateMaterialsSchema>;
563
- 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.
@@ -335,6 +336,7 @@ export const ProductTemplateManifestSchema = z
335
336
  allocatedPorts: z.array(ProductTemplateAllocatedPortSchema).optional().meta({
336
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.",
337
338
  }),
339
+ logs: ProductTemplateLogsSchema.optional(),
338
340
  migration: ProductTemplateMigrationSchema.optional().meta({
339
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.",
340
342
  }),
@@ -442,3 +444,4 @@ export const ProductTemplateMaterialsSchema = z
442
444
  }),
443
445
  })
444
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.26",
3
+ "version": "0.1.28",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {