@norskvideo/ctl-product-template-schema 0.1.60 → 0.1.62
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.js +7 -4
- package/package.json +5 -1
- package/secret-references.d.ts +27 -0
- package/secret-references.js +56 -0
- package/workdir-path.js +23 -10
package/index.js
CHANGED
|
@@ -601,11 +601,14 @@ const ProductTemplateFileSchema = z.object({
|
|
|
601
601
|
// subpath) turns this into tar bytes at the paths below.
|
|
602
602
|
//
|
|
603
603
|
// `workflow`, `components` and `dashboards` are known top-level paths in the
|
|
604
|
-
// v1 tar format. The runner detects studio in the compose stack and
|
|
605
|
-
// all three
|
|
606
|
-
// to declare the mounts.
|
|
604
|
+
// v1 tar format. The runner detects studio in the compose stack and places
|
|
605
|
+
// all three in the studio container automatically — products don't need
|
|
606
|
+
// to declare the mounts. workflow.yml is COPIED into the working directory on
|
|
607
|
+
// every launch (a single-file bind under /data is refused by OrbStack and
|
|
608
|
+
// Docker Desktop); the template stays authoritative, so a Studio save of it
|
|
609
|
+
// lasts only until the next launch.
|
|
607
610
|
//
|
|
608
|
-
// workflow.yml → /data/studio-save-files/workflow.yml (
|
|
611
|
+
// workflow.yml → /data/studio-save-files/workflow.yml (copied)
|
|
609
612
|
// components/<name>/ → /usr/src/app/plugins/<name>/ (RO)
|
|
610
613
|
// dashboards/<name>/ → /data/studio-save-files/<name>/dashboards/ (RO)
|
|
611
614
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-product-template-schema",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.62",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./migration-socket.d.ts",
|
|
37
37
|
"default": "./migration-socket.js"
|
|
38
38
|
},
|
|
39
|
+
"./secret-references": {
|
|
40
|
+
"types": "./secret-references.d.ts",
|
|
41
|
+
"default": "./secret-references.js"
|
|
42
|
+
},
|
|
39
43
|
"./product-channels": {
|
|
40
44
|
"types": "./product-channels.d.ts",
|
|
41
45
|
"default": "./product-channels.js"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Result } from "@norskvideo/ctl-foundation";
|
|
2
|
+
export declare const SECRET_REFERENCE_PREFIX = "secretref:";
|
|
3
|
+
/** The NodeInventory capability a worker advertises when it resolves secret
|
|
4
|
+
* references, so placement can keep a job whose parameters hold one off an
|
|
5
|
+
* older worker — which would hand the container the reference itself. */
|
|
6
|
+
export declare const SECRET_REFERENCES_CAPABILITY = "norsk.io/secret-references";
|
|
7
|
+
/** What a secret parameter's literal value is shown as wherever ctl prints
|
|
8
|
+
* launch parameters back. */
|
|
9
|
+
export declare const SECRET_PLACEHOLDER = "<secret>";
|
|
10
|
+
export interface SecretReference {
|
|
11
|
+
provider: string;
|
|
12
|
+
key: string;
|
|
13
|
+
}
|
|
14
|
+
/** A value is a reference exactly when it carries the prefix. A malformed
|
|
15
|
+
* reference is still a reference: it fails resolution rather than reaching a
|
|
16
|
+
* container as a literal. */
|
|
17
|
+
export declare function isSecretReference(value: string): boolean;
|
|
18
|
+
export declare function parseSecretReference(value: string): Result<SecretReference, string>;
|
|
19
|
+
export declare function secretReference(provider: string, key: string): string;
|
|
20
|
+
/** Launch parameters as they may be shown: a parameter the template declares
|
|
21
|
+
* `secret` has its literal value masked. A reference is shown as it is — it
|
|
22
|
+
* is the point of a reference that it reveals nothing — and an empty value
|
|
23
|
+
* stays visibly empty. */
|
|
24
|
+
export declare function redactSecretParams(params: Record<string, string>, declared: readonly {
|
|
25
|
+
name: string;
|
|
26
|
+
secret?: boolean;
|
|
27
|
+
}[]): Record<string, string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Secret references: a launch parameter's value may name a secret instead of
|
|
2
|
+
// carrying it, as `secretref:<provider>:<key>`. The worker that runs the
|
|
3
|
+
// instance resolves the reference immediately before it writes the
|
|
4
|
+
// container's environment, so a scheduler, its job store, its templates and
|
|
5
|
+
// its backups only ever hold the reference (norsk-ctl ADR-0017).
|
|
6
|
+
//
|
|
7
|
+
// Shared by both ends: whoever builds launch parameters (a Manager front end,
|
|
8
|
+
// a script) and the worker that resolves them. The provider names a resolver
|
|
9
|
+
// configured on the worker; the key is that provider's business and may hold
|
|
10
|
+
// colons, slashes or ciphertext.
|
|
11
|
+
import { err, ok } from "@norskvideo/ctl-foundation";
|
|
12
|
+
export const SECRET_REFERENCE_PREFIX = "secretref:";
|
|
13
|
+
/** The NodeInventory capability a worker advertises when it resolves secret
|
|
14
|
+
* references, so placement can keep a job whose parameters hold one off an
|
|
15
|
+
* older worker — which would hand the container the reference itself. */
|
|
16
|
+
export const SECRET_REFERENCES_CAPABILITY = "norsk.io/secret-references";
|
|
17
|
+
/** What a secret parameter's literal value is shown as wherever ctl prints
|
|
18
|
+
* launch parameters back. */
|
|
19
|
+
export const SECRET_PLACEHOLDER = "<secret>";
|
|
20
|
+
const PROVIDER_NAME = /^[a-z][a-z0-9-]*$/;
|
|
21
|
+
/** A value is a reference exactly when it carries the prefix. A malformed
|
|
22
|
+
* reference is still a reference: it fails resolution rather than reaching a
|
|
23
|
+
* container as a literal. */
|
|
24
|
+
export function isSecretReference(value) {
|
|
25
|
+
return value.startsWith(SECRET_REFERENCE_PREFIX);
|
|
26
|
+
}
|
|
27
|
+
export function parseSecretReference(value) {
|
|
28
|
+
if (!isSecretReference(value))
|
|
29
|
+
return err("not a secret reference");
|
|
30
|
+
const rest = value.slice(SECRET_REFERENCE_PREFIX.length);
|
|
31
|
+
const colon = rest.indexOf(":");
|
|
32
|
+
if (colon < 0)
|
|
33
|
+
return err(`a secret reference is ${SECRET_REFERENCE_PREFIX}<provider>:<key>`);
|
|
34
|
+
const provider = rest.slice(0, colon);
|
|
35
|
+
const key = rest.slice(colon + 1);
|
|
36
|
+
if (!PROVIDER_NAME.test(provider)) {
|
|
37
|
+
return err(`secret provider name '${provider}' must be lowercase letters, digits and dashes`);
|
|
38
|
+
}
|
|
39
|
+
if (key === "")
|
|
40
|
+
return err(`the secret reference for provider '${provider}' names no key`);
|
|
41
|
+
return ok({ provider, key });
|
|
42
|
+
}
|
|
43
|
+
export function secretReference(provider, key) {
|
|
44
|
+
return `${SECRET_REFERENCE_PREFIX}${provider}:${key}`;
|
|
45
|
+
}
|
|
46
|
+
/** Launch parameters as they may be shown: a parameter the template declares
|
|
47
|
+
* `secret` has its literal value masked. A reference is shown as it is — it
|
|
48
|
+
* is the point of a reference that it reveals nothing — and an empty value
|
|
49
|
+
* stays visibly empty. */
|
|
50
|
+
export function redactSecretParams(params, declared) {
|
|
51
|
+
const secret = new Set(declared.filter((p) => p.secret === true).map((p) => p.name));
|
|
52
|
+
return Object.fromEntries(Object.entries(params).map(([k, v]) => [
|
|
53
|
+
k,
|
|
54
|
+
secret.has(k) && v !== "" && !isSecretReference(v) ? SECRET_PLACEHOLDER : v,
|
|
55
|
+
]));
|
|
56
|
+
}
|
package/workdir-path.js
CHANGED
|
@@ -10,14 +10,19 @@
|
|
|
10
10
|
// Two separate hazards, deliberately not conflated:
|
|
11
11
|
// escape — `../../etc/x` writes outside the workdir. A hole.
|
|
12
12
|
// shadowing — a file written UNDER one of the runner's read-only bind
|
|
13
|
-
// mounts is on disk and invisible to the container
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
13
|
+
// mounts is on disk and invisible to the container, and a file
|
|
14
|
+
// at a path the launch writes itself is never the one used.
|
|
15
|
+
// Not a security problem, but a miserable one to debug, so it
|
|
16
|
+
// is refused at upload time rather than discovered at runtime.
|
|
17
|
+
/** Files the launch writes into the working directory itself. The product
|
|
18
|
+
* template's workflow.yml is copied to the path Studio's `server.document`
|
|
19
|
+
* names on every launch (norsk-ctl's workflow-sync.ts; it used to be a
|
|
20
|
+
* read-only single-file bind, which OrbStack and Docker Desktop refuse), so an
|
|
21
|
+
* operator asset there is replaced or skipped and never opened. */
|
|
22
|
+
const RUNNER_WRITTEN_FILES = ["studio-save-files/workflow.yml"];
|
|
23
|
+
/** Runner-managed read-only directory mount points inside the working
|
|
24
|
+
* directory (override-generator.ts); anything beneath them is shadowed. */
|
|
19
25
|
const READONLY_MOUNTS = {
|
|
20
|
-
file: ["studio-save-files/workflow.yml"],
|
|
21
26
|
dir: [".norsk"],
|
|
22
27
|
/** `studio-save-files/<anything>/dashboards` — the per-workflow dashboard mount. */
|
|
23
28
|
pattern: [/^studio-save-files\/[^/]+\/dashboards(\/|$)/],
|
|
@@ -46,9 +51,17 @@ export function checkWorkdirPath(raw) {
|
|
|
46
51
|
message: `'${path}' lands under the read-only mount '${mount}', where it would be on disk but invisible to the container`,
|
|
47
52
|
},
|
|
48
53
|
});
|
|
49
|
-
for (const file of
|
|
50
|
-
if (path === file)
|
|
51
|
-
return
|
|
54
|
+
for (const file of RUNNER_WRITTEN_FILES) {
|
|
55
|
+
if (path === file) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
problem: {
|
|
59
|
+
kind: "shadowed",
|
|
60
|
+
message: `'${path}' is where each launch copies the product template's workflow.yml, so an asset there would never be the one Studio opens`,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
52
65
|
for (const dir of READONLY_MOUNTS.dir)
|
|
53
66
|
if (path === dir || path.startsWith(`${dir}/`))
|
|
54
67
|
return shadowed(dir);
|