@telorun/kernel 0.42.0 → 0.44.0
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/dist/bundle/files-integrity.d.ts +25 -0
- package/dist/bundle/files-integrity.d.ts.map +1 -0
- package/dist/bundle/files-integrity.js +40 -0
- package/dist/bundle/files-integrity.js.map +1 -0
- package/dist/bundle/module-manifest.d.ts +22 -0
- package/dist/bundle/module-manifest.d.ts.map +1 -0
- package/dist/bundle/module-manifest.js +33 -0
- package/dist/bundle/module-manifest.js.map +1 -0
- package/dist/bundle/tar.d.ts +20 -0
- package/dist/bundle/tar.d.ts.map +1 -0
- package/dist/bundle/tar.js +63 -0
- package/dist/bundle/tar.js.map +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts +2 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +46 -0
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-inherited-controller.d.ts +21 -0
- package/dist/controllers/resource-definition/resource-inherited-controller.d.ts.map +1 -0
- package/dist/controllers/resource-definition/resource-inherited-controller.js +130 -0
- package/dist/controllers/resource-definition/resource-inherited-controller.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +11 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +21 -2
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.js +14 -93
- package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
- package/dist/resource-context.d.ts +8 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +10 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/transports/oci/docker-credentials.d.ts +10 -0
- package/dist/transports/oci/docker-credentials.d.ts.map +1 -0
- package/dist/transports/oci/docker-credentials.js +75 -0
- package/dist/transports/oci/docker-credentials.js.map +1 -0
- package/dist/transports/oci/oci-client.d.ts +51 -0
- package/dist/transports/oci/oci-client.d.ts.map +1 -0
- package/dist/transports/oci/oci-client.js +170 -0
- package/dist/transports/oci/oci-client.js.map +1 -0
- package/dist/transports/oci/oci-ref.d.ts +21 -0
- package/dist/transports/oci/oci-ref.d.ts.map +1 -0
- package/dist/transports/oci/oci-ref.js +36 -0
- package/dist/transports/oci/oci-ref.js.map +1 -0
- package/dist/transports/oci/oci-transport.d.ts +29 -0
- package/dist/transports/oci/oci-transport.d.ts.map +1 -0
- package/dist/transports/oci/oci-transport.js +142 -0
- package/dist/transports/oci/oci-transport.js.map +1 -0
- package/dist/transports/registry-transport.d.ts +23 -0
- package/dist/transports/registry-transport.d.ts.map +1 -0
- package/dist/transports/registry-transport.js +225 -0
- package/dist/transports/registry-transport.js.map +1 -0
- package/dist/transports/transport-registry.d.ts +40 -0
- package/dist/transports/transport-registry.d.ts.map +1 -0
- package/dist/transports/transport-registry.js +70 -0
- package/dist/transports/transport-registry.js.map +1 -0
- package/dist/transports/transport.d.ts +92 -0
- package/dist/transports/transport.d.ts.map +1 -0
- package/dist/transports/transport.js +2 -0
- package/dist/transports/transport.js.map +1 -0
- package/package.json +7 -4
- package/src/bundle/files-integrity.ts +46 -0
- package/src/bundle/module-manifest.ts +49 -0
- package/src/bundle/tar.ts +78 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +65 -0
- package/src/controllers/resource-definition/resource-inherited-controller.ts +167 -0
- package/src/index.ts +22 -0
- package/src/kernel.ts +24 -2
- package/src/manifest-sources/local-manifest-cache-source.ts +14 -99
- package/src/resource-context.ts +14 -0
- package/src/transports/oci/docker-credentials.ts +85 -0
- package/src/transports/oci/oci-client.ts +237 -0
- package/src/transports/oci/oci-ref.ts +53 -0
- package/src/transports/oci/oci-transport.ts +192 -0
- package/src/transports/registry-transport.ts +281 -0
- package/src/transports/transport-registry.ts +91 -0
- package/src/transports/transport.ts +112 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { extract as tarExtract, pack as tarPack } from "tar-stream";
|
|
2
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import type { PayloadFile } from "./files-integrity.js";
|
|
6
|
+
|
|
7
|
+
export interface BundleEntry {
|
|
8
|
+
/** POSIX-relative path inside the archive (e.g. `telo.yaml`, `public/app.js`). */
|
|
9
|
+
name: string;
|
|
10
|
+
content: Buffer | string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Normalize decoded tar entries to `PayloadFile`s (Buffer-backed content) —
|
|
14
|
+
* the fixed step both transports run after `readTarGz`. */
|
|
15
|
+
export function toPayloadFiles(entries: BundleEntry[]): PayloadFile[] {
|
|
16
|
+
return entries.map((e) => ({
|
|
17
|
+
name: e.name,
|
|
18
|
+
content: typeof e.content === "string" ? Buffer.from(e.content) : e.content,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pack `entries` into a gzipped tar (`module.tar.gz`) — the module-artifact
|
|
24
|
+
* writer shared by `telo publish` and the transports. Artifacts are small (a
|
|
25
|
+
* manifest plus a built frontend), so buffering the whole archive before gzip
|
|
26
|
+
* is fine. (Distinct from `apps/k8s-runner/src/tar.ts`, which is coupled to
|
|
27
|
+
* `@telorun/runner-core`'s `RunBundle`.)
|
|
28
|
+
*/
|
|
29
|
+
export async function makeTarGz(entries: BundleEntry[]): Promise<Buffer> {
|
|
30
|
+
const pack = tarPack();
|
|
31
|
+
const chunks: Buffer[] = [];
|
|
32
|
+
pack.on("data", (c: Buffer) => chunks.push(c));
|
|
33
|
+
|
|
34
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
35
|
+
pack.on("end", resolve);
|
|
36
|
+
pack.on("error", reject);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const buf = typeof entry.content === "string" ? Buffer.from(entry.content, "utf-8") : entry.content;
|
|
41
|
+
await new Promise<void>((resolve, reject) => {
|
|
42
|
+
pack.entry({ name: entry.name }, buf, (err) => (err ? reject(err) : resolve()));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
pack.finalize();
|
|
46
|
+
await done;
|
|
47
|
+
|
|
48
|
+
return gzipSync(Buffer.concat(chunks));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Decompress + untar a `module.tar.gz` buffer into its file entries. */
|
|
52
|
+
export async function readTarGz(buf: Buffer): Promise<BundleEntry[]> {
|
|
53
|
+
const tar = gunzipSync(buf);
|
|
54
|
+
const ex = tarExtract();
|
|
55
|
+
const entries: BundleEntry[] = [];
|
|
56
|
+
|
|
57
|
+
await new Promise<void>((resolve, reject) => {
|
|
58
|
+
ex.on("entry", (header, stream, next) => {
|
|
59
|
+
if (header.type !== "file") {
|
|
60
|
+
stream.on("end", next);
|
|
61
|
+
stream.resume();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const chunks: Buffer[] = [];
|
|
65
|
+
stream.on("data", (c: Buffer) => chunks.push(c));
|
|
66
|
+
stream.on("end", () => {
|
|
67
|
+
entries.push({ name: header.name, content: Buffer.concat(chunks) });
|
|
68
|
+
next();
|
|
69
|
+
});
|
|
70
|
+
stream.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
ex.on("finish", resolve);
|
|
73
|
+
ex.on("error", reject);
|
|
74
|
+
Readable.from(tar).pipe(ex);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ControllerContext,
|
|
3
3
|
ResourceContext,
|
|
4
|
+
ResourceDefinition as ResourceDefinitionManifest,
|
|
4
5
|
ResourceInstance,
|
|
5
6
|
RuntimeResource,
|
|
6
7
|
} from "@telorun/sdk";
|
|
8
|
+
import { RuntimeError } from "@telorun/sdk";
|
|
9
|
+
import {
|
|
10
|
+
controllerBearingAncestor,
|
|
11
|
+
hasOwnControllerOrTemplate,
|
|
12
|
+
inheritedCapability,
|
|
13
|
+
type DefResolver,
|
|
14
|
+
} from "@telorun/analyzer";
|
|
7
15
|
import { ControllerLoader } from "../../controller-loader.js";
|
|
8
16
|
import { formatAjvErrors, validateResourceDefinition } from "../../manifest-schemas.js";
|
|
9
17
|
import { createTemplateController } from "./resource-template-controller.js";
|
|
18
|
+
import { createInheritedController } from "./resource-inherited-controller.js";
|
|
10
19
|
|
|
11
20
|
type ResourceDefinitionResource = RuntimeResource & {
|
|
12
21
|
kind: "Telo.Definition";
|
|
@@ -17,6 +26,8 @@ type ResourceDefinitionResource = RuntimeResource & {
|
|
|
17
26
|
};
|
|
18
27
|
schema: Record<string, any>;
|
|
19
28
|
capability?: string;
|
|
29
|
+
extends?: string;
|
|
30
|
+
base?: Record<string, any>;
|
|
20
31
|
controllers?: Array<string>;
|
|
21
32
|
provide?: unknown;
|
|
22
33
|
};
|
|
@@ -31,6 +42,60 @@ class ResourceDefinition implements ResourceInstance {
|
|
|
31
42
|
constructor(readonly resource: ResourceDefinitionResource) {}
|
|
32
43
|
|
|
33
44
|
async init(ctx: ResourceContext) {
|
|
45
|
+
const definingCtx = ctx.moduleContext;
|
|
46
|
+
// Resolve an `extends`/kind target (alias or canonical form) to its
|
|
47
|
+
// definition against the DEFINING library's scope — where the `extends`
|
|
48
|
+
// alias was declared.
|
|
49
|
+
const resolveDef: DefResolver = (kind) => {
|
|
50
|
+
let canonical = kind;
|
|
51
|
+
try {
|
|
52
|
+
canonical = definingCtx.resolveKind(kind);
|
|
53
|
+
} catch {
|
|
54
|
+
// ungated / unqualified — fall back to the raw kind below
|
|
55
|
+
}
|
|
56
|
+
return definingCtx.getDefinition?.(canonical) ?? definingCtx.getDefinition?.(kind);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Inherited-controller delegation: a definition that `extends` a concrete
|
|
60
|
+
// kind, declares no own `controllers:` / template body, inherits the parent
|
|
61
|
+
// controller by delegation and maps its config via `base:`.
|
|
62
|
+
if (this.resource.extends && !hasOwnControllerOrTemplate(this.resource as ResourceDefinitionManifest)) {
|
|
63
|
+
const parentDef = resolveDef(this.resource.extends);
|
|
64
|
+
if (!parentDef) {
|
|
65
|
+
// The parent's Telo.Definition (and thus its controller) isn't loaded
|
|
66
|
+
// yet — defer; the multi-pass init loop retries once its import resolves.
|
|
67
|
+
throw new RuntimeError(
|
|
68
|
+
"ERR_LOCAL_REF_PENDING",
|
|
69
|
+
`Telo.Definition '${this.resource.metadata.name}': 'extends' target '${this.resource.extends}' is not loaded yet.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (controllerBearingAncestor(this.resource as ResourceDefinitionManifest, resolveDef)) {
|
|
73
|
+
// Capability is inherited and immutable — stamp the resolved capability
|
|
74
|
+
// so every consumer that reads `definition.capability` (compile-CEL eval
|
|
75
|
+
// paths, `capabilityOf`, lifecycle role) sees the effective role without
|
|
76
|
+
// re-walking `extends`.
|
|
77
|
+
if (!this.resource.capability) {
|
|
78
|
+
const inherited = inheritedCapability(this.resource as ResourceDefinitionManifest, resolveDef);
|
|
79
|
+
if (inherited) this.resource.capability = inherited;
|
|
80
|
+
}
|
|
81
|
+
const controllerInstance = createInheritedController(
|
|
82
|
+
this.resource as ResourceDefinitionManifest,
|
|
83
|
+
definingCtx,
|
|
84
|
+
resolveDef,
|
|
85
|
+
);
|
|
86
|
+
ctx.registerDefinition(this.resource);
|
|
87
|
+
await ctx.registerController(
|
|
88
|
+
this.resource.metadata.module,
|
|
89
|
+
this.resource.metadata.name,
|
|
90
|
+
controllerInstance,
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Otherwise the chain reaches no controller-bearing concrete ancestor
|
|
95
|
+
// (e.g. it implements a pure abstract) — fall through to the normal
|
|
96
|
+
// template / controller handling below.
|
|
97
|
+
}
|
|
98
|
+
|
|
34
99
|
if (!this.resource.controllers?.length) {
|
|
35
100
|
if (this.resource.capability === "Telo.Provider" && this.resource.provide == null) {
|
|
36
101
|
throw new Error(
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ControllerInstance,
|
|
3
|
+
EvaluationContext,
|
|
4
|
+
ResourceContext,
|
|
5
|
+
ResourceDefinition,
|
|
6
|
+
ResourceInstance,
|
|
7
|
+
} from "@telorun/sdk";
|
|
8
|
+
import { isCompiledValue } from "@telorun/sdk";
|
|
9
|
+
import {
|
|
10
|
+
buildReferenceFieldMap,
|
|
11
|
+
effectiveAuthorSchema,
|
|
12
|
+
isRefEntry,
|
|
13
|
+
type DefResolver,
|
|
14
|
+
} from "@telorun/analyzer";
|
|
15
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
16
|
+
|
|
17
|
+
/** Typed internal seam implemented by the concrete `ResourceContextImpl`: runs
|
|
18
|
+
* the kernel's create phase for the parent-kind manifest and returns the native
|
|
19
|
+
* parent instance (or null when the parent controller isn't registered yet — a
|
|
20
|
+
* retry, same as `_createInstance`). Off the public SDK surface, like
|
|
21
|
+
* `registerLazyController`. */
|
|
22
|
+
export interface InheritedInstanceHost {
|
|
23
|
+
createInheritedInstance(
|
|
24
|
+
evalContext: EvaluationContext,
|
|
25
|
+
resource: Record<string, unknown>,
|
|
26
|
+
): Promise<ResourceInstance | null>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Matches a CEL source that is exactly a `self.<path>` member access — resolved
|
|
30
|
+
* by direct navigation so live resource instances (which CEL's output type
|
|
31
|
+
* checker rejects) can flow through `base:` untouched. */
|
|
32
|
+
const SELF_PATH = /^self((?:\.[A-Za-z_$][\w$]*)+)$/;
|
|
33
|
+
|
|
34
|
+
interface Snapshotable {
|
|
35
|
+
snapshot(): Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
const hasSnapshot = (v: unknown): v is Snapshotable =>
|
|
38
|
+
!!v && typeof (v as { snapshot?: unknown }).snapshot === "function";
|
|
39
|
+
|
|
40
|
+
/** Resolves a reference-slot value to its live instance, or undefined when the
|
|
41
|
+
* referenced resource isn't initialized yet. A value that is already a live
|
|
42
|
+
* instance (has `snapshot()`) passes through. Post Phase-2.5 a slot holds a
|
|
43
|
+
* `{kind, name, alias?}` object; a raw `!ref` sentinel (only reachable behind a
|
|
44
|
+
* hidden `$ref` slot) is normalized through the sanctioned `ctx.resolveChildren`
|
|
45
|
+
* so the reference grammar (alias/Self splitting) stays in one place. */
|
|
46
|
+
function resolveRefSlot(value: unknown, ctx: ResourceContext): ResourceInstance | undefined {
|
|
47
|
+
if (hasSnapshot(value)) return value as unknown as ResourceInstance;
|
|
48
|
+
let name: string | undefined;
|
|
49
|
+
let alias: string | undefined;
|
|
50
|
+
if (isRefSentinel(value)) {
|
|
51
|
+
const ref = ctx.resolveChildren(value) as { name: string; alias?: string };
|
|
52
|
+
name = ref.name;
|
|
53
|
+
alias = ref.alias;
|
|
54
|
+
} else if (value && typeof value === "object") {
|
|
55
|
+
const ref = value as { name?: unknown; alias?: unknown };
|
|
56
|
+
if (typeof ref.name === "string") {
|
|
57
|
+
name = ref.name;
|
|
58
|
+
alias = typeof ref.alias === "string" ? ref.alias : undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!name) return undefined;
|
|
62
|
+
const instance =
|
|
63
|
+
alias && alias !== "Self"
|
|
64
|
+
? ctx.moduleContext.resolveImportedInstance(alias, name)
|
|
65
|
+
: (ctx.moduleContext.resourceInstances.get(name)?.instance as ResourceInstance | undefined);
|
|
66
|
+
return instance ?? undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Expand a `base:` node against `self`. Self-only CEL resolves to literals now;
|
|
70
|
+
* a pure `self.<path>` access is navigated directly so live instances pass
|
|
71
|
+
* through. */
|
|
72
|
+
function expandBaseNode(value: unknown, self: Record<string, unknown>, ctx: EvaluationContext): unknown {
|
|
73
|
+
if (isCompiledValue(value)) {
|
|
74
|
+
const src = typeof value.source === "string" ? value.source.trim() : "";
|
|
75
|
+
const m = src.match(SELF_PATH);
|
|
76
|
+
if (m) {
|
|
77
|
+
let cur: unknown = self;
|
|
78
|
+
for (const key of m[1].split(".").slice(1)) {
|
|
79
|
+
cur = (cur as Record<string, unknown> | undefined)?.[key];
|
|
80
|
+
}
|
|
81
|
+
return cur;
|
|
82
|
+
}
|
|
83
|
+
return ctx.expandWith(value, { self });
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(value)) return value.map((v) => expandBaseNode(v, self, ctx));
|
|
86
|
+
if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
87
|
+
const out: Record<string, unknown> = {};
|
|
88
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
89
|
+
out[k] = expandBaseNode(v, self, ctx);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Controller for a definition that inherits its controller by delegation — it
|
|
98
|
+
* `extends` a concrete kind, declares no own `controllers:` / template body, and
|
|
99
|
+
* maps the parent's config via `base:`. `create()` evaluates `base:` against the
|
|
100
|
+
* child instance's config (`self`, with reference slots resolved to live
|
|
101
|
+
* instances) and constructs the parent kind through {@link InheritedInstanceHost},
|
|
102
|
+
* returning that native parent instance verbatim so the child duck-types as its
|
|
103
|
+
* parent (e.g. inherits `.snapshot()`).
|
|
104
|
+
*/
|
|
105
|
+
export function createInheritedController(
|
|
106
|
+
definition: ResourceDefinition,
|
|
107
|
+
definingContext: EvaluationContext,
|
|
108
|
+
resolveDef: DefResolver,
|
|
109
|
+
): ControllerInstance {
|
|
110
|
+
const authorSchema = effectiveAuthorSchema(definition, resolveDef);
|
|
111
|
+
const parentKind = definition.extends;
|
|
112
|
+
if (!parentKind) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Telo.Definition '${definition.metadata.name}': inherited controller requires an 'extends' target.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const base = definition.base;
|
|
118
|
+
|
|
119
|
+
// Top-level reference fields on the author-facing schema — resolved to live
|
|
120
|
+
// instances before `base:` runs so a `!cel "self.<ref>"` passthrough forwards
|
|
121
|
+
// the live parent instance (e.g. a `client` into an inherited Http.Request).
|
|
122
|
+
const refFieldPaths: string[] = [];
|
|
123
|
+
for (const [path, entry] of buildReferenceFieldMap(authorSchema)) {
|
|
124
|
+
if (isRefEntry(entry) && !path.includes(".") && !path.includes("[")) refFieldPaths.push(path);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
schema: authorSchema,
|
|
129
|
+
|
|
130
|
+
create: async (resource: any, ctx: ResourceContext): Promise<ResourceInstance | null> => {
|
|
131
|
+
const self: Record<string, unknown> = { ...resource, name: resource.metadata.name };
|
|
132
|
+
for (const path of refFieldPaths) {
|
|
133
|
+
const raw = (resource as Record<string, unknown>)[path];
|
|
134
|
+
if (raw == null) continue;
|
|
135
|
+
const live = resolveRefSlot(raw, ctx);
|
|
136
|
+
// A reference that is set but hasn't resolved yet means the dependency
|
|
137
|
+
// isn't initialized — defer via the retry signal, like _createInstance.
|
|
138
|
+
if (!live) return null;
|
|
139
|
+
self[path] = live;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// With `base:`, it maps `self` onto the parent's (narrowed) config. Without
|
|
143
|
+
// it, the child is a pure additive extension: it carries the parent's config
|
|
144
|
+
// fields directly, so forward them (reference slots already resolved to live
|
|
145
|
+
// instances in `self`), minus the reserved keys.
|
|
146
|
+
let parentConfig: Record<string, unknown>;
|
|
147
|
+
if (base != null) {
|
|
148
|
+
parentConfig = expandBaseNode(base, self, definingContext) as Record<string, unknown>;
|
|
149
|
+
} else {
|
|
150
|
+
const { kind: _kind, metadata: _metadata, name: _name, ...config } = self;
|
|
151
|
+
parentConfig = config;
|
|
152
|
+
}
|
|
153
|
+
const parentResource: Record<string, unknown> = {
|
|
154
|
+
kind: parentKind,
|
|
155
|
+
metadata: { ...resource.metadata },
|
|
156
|
+
...parentConfig,
|
|
157
|
+
};
|
|
158
|
+
const host = ctx as unknown as InheritedInstanceHost;
|
|
159
|
+
if (typeof (host as { createInheritedInstance?: unknown }).createInheritedInstance !== "function") {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Telo.Definition '${definition.metadata.name}': inherited controller requires a ResourceContext host that implements createInheritedInstance().`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return host.createInheritedInstance(definingContext, parentResource);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,28 @@ export {
|
|
|
10
10
|
writeManifestCache,
|
|
11
11
|
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
12
12
|
export { MemorySource } from "./manifest-sources/memory-source.js";
|
|
13
|
+
export type { Transport } from "./transports/transport.js";
|
|
14
|
+
export { RegistryTransport } from "./transports/registry-transport.js";
|
|
15
|
+
export { OciTransport } from "./transports/oci/oci-transport.js";
|
|
16
|
+
export { OciClient } from "./transports/oci/oci-client.js";
|
|
17
|
+
export { isOciRef, parseOciRef, type ParsedOciRef } from "./transports/oci/oci-ref.js";
|
|
18
|
+
export {
|
|
19
|
+
TransportRegistry,
|
|
20
|
+
defaultTransports,
|
|
21
|
+
defaultTransportRegistry,
|
|
22
|
+
} from "./transports/transport-registry.js";
|
|
23
|
+
export { makeTarGz, readTarGz, type BundleEntry } from "./bundle/tar.js";
|
|
24
|
+
export {
|
|
25
|
+
computeFilesIntegrity,
|
|
26
|
+
injectFilesIntegrity,
|
|
27
|
+
type PayloadFile,
|
|
28
|
+
} from "./bundle/files-integrity.js";
|
|
29
|
+
export type {
|
|
30
|
+
FetchedArtifact,
|
|
31
|
+
PublishBundle,
|
|
32
|
+
PublishResult,
|
|
33
|
+
PublishOptions,
|
|
34
|
+
} from "./transports/transport.js";
|
|
13
35
|
export { ExecutionContext } from "./execution-context.js";
|
|
14
36
|
export { Kernel, type KernelOptions } from "./kernel.js";
|
|
15
37
|
export { nodeCelHandlers } from "./cel-handlers.js";
|
package/src/kernel.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnalysisRegistry,
|
|
3
3
|
buildEvalPaths,
|
|
4
|
-
defaultSources,
|
|
5
4
|
flattenForAnalyzer,
|
|
6
5
|
flattenLoadedModule,
|
|
7
6
|
isModuleKind,
|
|
@@ -48,6 +47,7 @@ import {
|
|
|
48
47
|
import {
|
|
49
48
|
resolveCacheRoot,
|
|
50
49
|
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
50
|
+
import { defaultTransportRegistry } from "./transports/transport-registry.js";
|
|
51
51
|
import {
|
|
52
52
|
collectDeclaredEnvKeys,
|
|
53
53
|
precompileApplicationEnvSchemas,
|
|
@@ -156,7 +156,12 @@ export class Kernel implements IKernel {
|
|
|
156
156
|
this.env = options.env ?? hostEnv();
|
|
157
157
|
this.argv = options.argv ?? [];
|
|
158
158
|
this.registryUrl = options.registryUrl;
|
|
159
|
-
|
|
159
|
+
// Resolution sources come from the transport registry, so a scheme-owning
|
|
160
|
+
// transport (OCI, later S3) joins the loader's dispatch chain by being
|
|
161
|
+
// registered — no source-chain edits here.
|
|
162
|
+
this.loader = new Loader(defaultTransportRegistry(this.registryUrl).sources(), {
|
|
163
|
+
celHandlers: nodeCelHandlers,
|
|
164
|
+
});
|
|
160
165
|
for (const source of options.sources) {
|
|
161
166
|
this.loader.register(source);
|
|
162
167
|
}
|
|
@@ -1113,6 +1118,23 @@ export class Kernel implements IKernel {
|
|
|
1113
1118
|
return { instance, ctx, resource: processedResource };
|
|
1114
1119
|
}
|
|
1115
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Create phase for an inherited (concrete-`extends`) definition's parent: runs
|
|
1123
|
+
* the ordinary create pipeline (controller resolution + lazy load, schema
|
|
1124
|
+
* validation, compile-CEL expansion, ctx creation) for the parent-kind
|
|
1125
|
+
* manifest built from `base:`, and returns the native parent instance. The
|
|
1126
|
+
* inherited child's controller returns this verbatim so the child duck-types
|
|
1127
|
+
* as its parent. Null means the parent controller isn't registered yet — the
|
|
1128
|
+
* caller propagates it as the create-phase retry signal.
|
|
1129
|
+
*/
|
|
1130
|
+
async createInheritedInstance(
|
|
1131
|
+
evalContext: IEvaluationContext,
|
|
1132
|
+
resource: ResourceManifest,
|
|
1133
|
+
): Promise<ResourceInstance | null> {
|
|
1134
|
+
const created = await this._createInstance(evalContext, resource);
|
|
1135
|
+
return created?.instance ?? null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1116
1138
|
/**
|
|
1117
1139
|
* Phase 5 — Inject live instances into reference fields of a resource config.
|
|
1118
1140
|
*
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { splitIntegrity, verifyIntegrity, type LoadedGraph, type ManifestSource } from "@telorun/analyzer";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
2
|
import { statSync } from "fs";
|
|
4
3
|
import * as fs from "fs/promises";
|
|
5
4
|
import * as path from "path";
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
7
6
|
|
|
8
7
|
import { hostEnv } from "../host-env.js";
|
|
8
|
+
import { TransportRegistry, defaultTransportRegistry } from "../transports/transport-registry.js";
|
|
9
9
|
|
|
10
10
|
const CACHE_SUBDIR = ".telo/manifests";
|
|
11
|
-
const HTTP_NAMESPACE = "__http";
|
|
12
|
-
const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
13
11
|
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
14
|
-
const QUERY_HASH_LENGTH = 12;
|
|
15
12
|
|
|
16
13
|
/** Verify that `candidate` resolves to a path under `root`. Returns the
|
|
17
14
|
* candidate path on success, `null` when any segment escapes the root.
|
|
@@ -27,104 +24,22 @@ function joinUnder(root: string, ...segments: string[]): string | null {
|
|
|
27
24
|
return candidate;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
/** Mirror `HttpSource.read`'s `fetchUrl` derivation: when the URL does not
|
|
31
|
-
* already point at a YAML file, append `/telo.yaml`. Used by both the
|
|
32
|
-
* reader and writer so a raw import URL and the canonical source it
|
|
33
|
-
* resolves to map to the same cache path. */
|
|
34
|
-
function normalizePathname(url: string, parsed: URL): string {
|
|
35
|
-
let pathname = parsed.pathname;
|
|
36
|
-
if (!url.includes(".yaml")) {
|
|
37
|
-
pathname = pathname.endsWith("/")
|
|
38
|
-
? `${pathname}${DEFAULT_MANIFEST_FILENAME}`
|
|
39
|
-
: `${pathname}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
40
|
-
}
|
|
41
|
-
return pathname;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Compute a short hash of `search + hash` so two URLs that differ only in
|
|
45
|
-
* query / fragment do not collide at the same cache path. Inserted before
|
|
46
|
-
* the final extension so the filename stays readable. */
|
|
47
|
-
function disambiguatePath(pathname: string, search: string, hash: string): string {
|
|
48
|
-
if (!search && !hash) return pathname;
|
|
49
|
-
const digest = createHash("sha256")
|
|
50
|
-
.update(search + hash)
|
|
51
|
-
.digest("hex")
|
|
52
|
-
.slice(0, QUERY_HASH_LENGTH);
|
|
53
|
-
const ext = path.extname(pathname);
|
|
54
|
-
const base = pathname.slice(0, pathname.length - ext.length);
|
|
55
|
-
return `${base}.${digest}${ext}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
27
|
/** Single source of truth for URL → cache path. Used identically by the
|
|
59
28
|
* reader (cache lookup) and writer (install-time persistence). For any
|
|
60
|
-
* given import
|
|
61
|
-
*
|
|
29
|
+
* given import ref — registry ref, direct registry URL, arbitrary HTTP, or
|
|
30
|
+
* (once registered) `oci://` — both sides land on the same file, because the
|
|
31
|
+
* owning transport decides the layout via {@link Transport.cacheLocation}.
|
|
62
32
|
*
|
|
63
|
-
* Returns `null` for unsupported
|
|
64
|
-
*
|
|
33
|
+
* Returns `null` for unsupported refs (file://, memory://, relative paths) or
|
|
34
|
+
* for path-traversal attempts that would escape `cacheRoot`. */
|
|
65
35
|
function cachePathForUrl(
|
|
66
36
|
rawUrl: string,
|
|
67
37
|
cacheRoot: string,
|
|
68
|
-
|
|
38
|
+
transports: TransportRegistry,
|
|
69
39
|
): string | null {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const trimmedRegistry = registryUrl.replace(/\/+$/, "");
|
|
74
|
-
|
|
75
|
-
// 1. Registry ref form: namespace/name@version
|
|
76
|
-
if (
|
|
77
|
-
!url.startsWith("http://") &&
|
|
78
|
-
!url.startsWith("https://") &&
|
|
79
|
-
!url.startsWith("/") &&
|
|
80
|
-
!url.startsWith(".") &&
|
|
81
|
-
!url.startsWith("file://") &&
|
|
82
|
-
!url.startsWith("memory://") &&
|
|
83
|
-
url.includes("@") &&
|
|
84
|
-
url.includes("/")
|
|
85
|
-
) {
|
|
86
|
-
const atIdx = url.lastIndexOf("@");
|
|
87
|
-
if (atIdx <= 0 || atIdx === url.length - 1) return null;
|
|
88
|
-
const modulePath = url.slice(0, atIdx);
|
|
89
|
-
if (!modulePath.includes("/")) return null;
|
|
90
|
-
const version = url.slice(atIdx + 1).replace(/^v/, "");
|
|
91
|
-
if (!version) return null;
|
|
92
|
-
return joinUnder(cacheRoot, modulePath, version, DEFAULT_MANIFEST_FILENAME);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 2. HTTP(S) URL — could be a direct registry URL or arbitrary external.
|
|
96
|
-
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
97
|
-
let parsed: URL;
|
|
98
|
-
try {
|
|
99
|
-
parsed = new URL(url);
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const pathname = normalizePathname(url, parsed);
|
|
104
|
-
|
|
105
|
-
// 2a. URL is on the configured registry, with no query/fragment:
|
|
106
|
-
// fold into the registry layout so the writer that received a
|
|
107
|
-
// registry-ref canonical and the reader that sees a direct URL
|
|
108
|
-
// both resolve to the same file.
|
|
109
|
-
const normalizedUrl = `${parsed.protocol}//${parsed.host}${pathname}`;
|
|
110
|
-
if (
|
|
111
|
-
!parsed.search &&
|
|
112
|
-
!parsed.hash &&
|
|
113
|
-
(normalizedUrl === trimmedRegistry || normalizedUrl.startsWith(`${trimmedRegistry}/`))
|
|
114
|
-
) {
|
|
115
|
-
const rel = normalizedUrl.slice(trimmedRegistry.length + 1);
|
|
116
|
-
if (!rel) return null;
|
|
117
|
-
return joinUnder(cacheRoot, ...rel.split("/"));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 2b. Arbitrary HTTP(S) URL → __http subtree, with a short query-hash
|
|
121
|
-
// suffix when query / fragment are present to prevent collisions.
|
|
122
|
-
const cleanPath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
123
|
-
const disambiguated = disambiguatePath(cleanPath, parsed.search, parsed.hash);
|
|
124
|
-
return joinUnder(cacheRoot, HTTP_NAMESPACE, parsed.host, ...disambiguated.split("/"));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return null;
|
|
40
|
+
const segments = transports.cacheLocation(rawUrl);
|
|
41
|
+
if (!segments) return null;
|
|
42
|
+
return joinUnder(cacheRoot, ...segments);
|
|
128
43
|
}
|
|
129
44
|
|
|
130
45
|
/**
|
|
@@ -136,7 +51,7 @@ function cachePathForUrl(
|
|
|
136
51
|
*/
|
|
137
52
|
export class LocalManifestCacheSource implements ManifestSource {
|
|
138
53
|
private readonly cacheRoot: string;
|
|
139
|
-
private readonly
|
|
54
|
+
private readonly transports: TransportRegistry;
|
|
140
55
|
|
|
141
56
|
constructor(
|
|
142
57
|
entryDir: string,
|
|
@@ -147,7 +62,7 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
147
62
|
// single `resolveCacheRoot` (honours `TELO_CACHE_DIR`); when absent we fall
|
|
148
63
|
// back to the entry-anchored default so library/test callers are unchanged.
|
|
149
64
|
this.cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
|
|
150
|
-
this.
|
|
65
|
+
this.transports = defaultTransportRegistry(registryUrl);
|
|
151
66
|
}
|
|
152
67
|
|
|
153
68
|
supports(url: string): boolean {
|
|
@@ -184,7 +99,7 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
184
99
|
}
|
|
185
100
|
|
|
186
101
|
private tryMap(url: string): string | null {
|
|
187
|
-
const candidate = cachePathForUrl(url, this.cacheRoot, this.
|
|
102
|
+
const candidate = cachePathForUrl(url, this.cacheRoot, this.transports);
|
|
188
103
|
if (!candidate) return null;
|
|
189
104
|
// Require a regular file. A directory, dangling symlink, or stat failure
|
|
190
105
|
// (ENOENT, EACCES, EISDIR-on-component) all fall through as a cache miss
|
|
@@ -213,7 +128,7 @@ export function cachePathForCanonical(
|
|
|
213
128
|
manifestsDir?: string,
|
|
214
129
|
): string | null {
|
|
215
130
|
const cacheRoot = manifestsDir ?? path.join(entryDir, CACHE_SUBDIR);
|
|
216
|
-
return cachePathForUrl(canonicalSource, cacheRoot, registryUrl);
|
|
131
|
+
return cachePathForUrl(canonicalSource, cacheRoot, defaultTransportRegistry(registryUrl));
|
|
217
132
|
}
|
|
218
133
|
|
|
219
134
|
/**
|
package/src/resource-context.ts
CHANGED
|
@@ -412,6 +412,20 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
412
412
|
this.kernel.registerLazyController(moduleName, kindName, fingerprint, load);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Run the create phase for an inherited (concrete-`extends`) definition's
|
|
417
|
+
* parent kind, returning the native parent instance (or null when the parent
|
|
418
|
+
* controller isn't registered yet — a retry). A typed internal seam for the
|
|
419
|
+
* inherited-controller delegation; off the public SDK `ResourceContext`
|
|
420
|
+
* surface, like `registerLazyController`.
|
|
421
|
+
*/
|
|
422
|
+
createInheritedInstance(
|
|
423
|
+
evalContext: IEvaluationContext,
|
|
424
|
+
resource: Record<string, unknown>,
|
|
425
|
+
): Promise<ResourceInstance | null> {
|
|
426
|
+
return this.kernel.createInheritedInstance(evalContext, resource as ResourceManifest);
|
|
427
|
+
}
|
|
428
|
+
|
|
415
429
|
registerDefinition(def: any) {
|
|
416
430
|
this.kernel.registerResourceDefinition(def);
|
|
417
431
|
}
|