@norskvideo/ctl-product-template-schema 0.1.26 → 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 +41 -3
- package/index.js +3 -0
- package/logs.d.ts +86 -0
- package/logs.js +116 -0
- package/package.json +1 -1
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
|
+
};
|