@intentius/chant-lexicon-docker 0.124.46 → 0.124.48

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.
Files changed (39) hide show
  1. package/dist/export-resources.d.ts +21 -0
  2. package/dist/export-resources.d.ts.map +1 -0
  3. package/dist/export-resources.js +63 -0
  4. package/dist/import/adapter.d.ts +6 -2
  5. package/dist/import/adapter.d.ts.map +1 -1
  6. package/dist/import/adapter.js +11 -7
  7. package/dist/import/embedded.d.ts +27 -0
  8. package/dist/import/embedded.d.ts.map +1 -0
  9. package/dist/import/embedded.js +79 -0
  10. package/dist/import/engine.d.ts +39 -0
  11. package/dist/import/engine.d.ts.map +1 -0
  12. package/dist/import/engine.js +94 -0
  13. package/dist/import/generator.d.ts.map +1 -1
  14. package/dist/import/generator.js +41 -10
  15. package/dist/import/live-export.d.ts +142 -0
  16. package/dist/import/live-export.d.ts.map +1 -0
  17. package/dist/import/live-export.js +297 -0
  18. package/dist/import/parser.d.ts +9 -1
  19. package/dist/import/parser.d.ts.map +1 -1
  20. package/dist/import/parser.js +19 -2
  21. package/dist/import/testdata/tar.d.ts +3 -0
  22. package/dist/import/testdata/tar.d.ts.map +1 -0
  23. package/dist/import/testdata/tar.js +25 -0
  24. package/dist/integrity.json +2 -2
  25. package/dist/manifest.json +1 -1
  26. package/dist/plugin.d.ts.map +1 -1
  27. package/dist/plugin.js +5 -0
  28. package/package.json +3 -3
  29. package/src/export-resources.ts +90 -0
  30. package/src/import/adapter.ts +14 -9
  31. package/src/import/embedded-live.test.ts +248 -0
  32. package/src/import/embedded.ts +83 -0
  33. package/src/import/engine.ts +107 -0
  34. package/src/import/generator.ts +42 -10
  35. package/src/import/live-export.test.ts +271 -0
  36. package/src/import/live-export.ts +359 -0
  37. package/src/import/parser.ts +19 -2
  38. package/src/import/testdata/tar.ts +24 -0
  39. package/src/plugin.ts +6 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Live export for the docker lexicon (#4388): implements
3
+ * `LexiconPlugin.exportResources()` so `chant import --from <env>` regenerates
4
+ * a compose project's services, and the configs they read, as chant
5
+ * TypeScript.
6
+ *
7
+ * Which project: `COMPOSE_PROJECT_NAME` when set, else the environment name.
8
+ * The containers are found by compose's `com.docker.compose.project` label.
9
+ * All Engine I/O lives here; mapping is pure in `./import/live-export`.
10
+ */
11
+ import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
12
+ import { type DockerEngine } from "./import/engine.js";
13
+ /** The compose project an environment exports. */
14
+ export declare function projectFor(environment: string): string;
15
+ export declare function exportResources(options: {
16
+ environment: string;
17
+ selector?: ResourceSelector;
18
+ owned?: boolean;
19
+ verbatim?: boolean;
20
+ }, engine?: DockerEngine, signal?: AbortSignal): Promise<ExportedTemplate>;
21
+ //# sourceMappingURL=export-resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-resources.d.ts","sourceRoot":"","sources":["../src/export-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAuC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAoBzF,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE;IACP,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,EACD,MAAM,GAAE,YAAoC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CA2C3B"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Live export for the docker lexicon (#4388): implements
3
+ * `LexiconPlugin.exportResources()` so `chant import --from <env>` regenerates
4
+ * a compose project's services, and the configs they read, as chant
5
+ * TypeScript.
6
+ *
7
+ * Which project: `COMPOSE_PROJECT_NAME` when set, else the environment name.
8
+ * The containers are found by compose's `com.docker.compose.project` label.
9
+ * All Engine I/O lives here; mapping is pure in `./import/live-export`.
10
+ */
11
+ import { defaultDockerEngine, singleFileText } from "./import/engine.js";
12
+ import { buildExportFromProject, configCandidates, isChantOwned, PROJECT_LABEL, serviceContainers, } from "./import/live-export.js";
13
+ function json(body) {
14
+ try {
15
+ return JSON.parse(body.toString("utf8"));
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ /** The compose project an environment exports. */
22
+ export function projectFor(environment) {
23
+ return process.env.COMPOSE_PROJECT_NAME || environment;
24
+ }
25
+ export async function exportResources(options, engine = defaultDockerEngine(), signal) {
26
+ const project = projectFor(options.environment);
27
+ const filters = encodeURIComponent(JSON.stringify({ label: [`${PROJECT_LABEL}=${project}`] }));
28
+ const list = await engine(`/containers/json?all=1&filters=${filters}`, signal);
29
+ if (list.status !== 200) {
30
+ throw new Error(`docker live export: listing the containers of compose project ${project} failed (${list.status}): ${list.body.toString("utf8").trim()}`);
31
+ }
32
+ const containers = serviceContainers(project, json(list.body) ?? []);
33
+ const services = [];
34
+ const images = new Map();
35
+ for (const [name, id] of containers) {
36
+ const res = await engine(`/containers/${encodeURIComponent(id)}/json`, signal);
37
+ const container = res.status === 200 ? json(res.body) : undefined;
38
+ if (!container)
39
+ continue;
40
+ const ref = container.Config?.Image;
41
+ if (ref && !images.has(ref)) {
42
+ const img = await engine(`/images/${encodeURIComponent(ref)}/json`, signal);
43
+ images.set(ref, img.status === 200 ? json(img.body) : undefined);
44
+ }
45
+ // Config files are read only for the services that are exported.
46
+ const files = {};
47
+ if (!options.owned || isChantOwned(container)) {
48
+ for (const path of configCandidates(container)) {
49
+ const res = await engine(`/containers/${encodeURIComponent(id)}/archive?path=${encodeURIComponent(path)}`, signal);
50
+ if (res.status !== 200)
51
+ continue;
52
+ const text = singleFileText(res.body);
53
+ if (text !== undefined)
54
+ files[path] = text;
55
+ }
56
+ }
57
+ services.push({ name, container, image: ref ? images.get(ref) : undefined, files });
58
+ }
59
+ if (options.owned) {
60
+ console.warn("[docker] the docker serializer stamps no ownership marker; under --owned only services labelled app.kubernetes.io/managed-by=chant (defaultLabels) are exported");
61
+ }
62
+ return buildExportFromProject(project, services, options);
63
+ }
@@ -10,15 +10,19 @@
10
10
  * keyed by its `kind` (service/volume/network/config/secret/dockerfile) and
11
11
  * reverse the mapping in the generator so DockerGenerator can do the real work.
12
12
  */
13
- import type { TemplateIR, TemplateParser } from "@intentius/chant/import/parser";
13
+ import type { TemplateIR, TemplateParser, ParseContext } from "@intentius/chant/import/parser";
14
14
  import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
15
+ import type { ParseResult } from "./parser.js";
15
16
  /**
16
17
  * TemplateParser adapter — wraps DockerParser.parse and maps its DockerIR
17
18
  * entities to a TemplateIR.
18
19
  */
19
20
  export declare class DockerTemplateParser implements TemplateParser {
20
- parse(content: string): TemplateIR;
21
+ /** With `context.embedded`, a config's `content` another lexicon owns is handed to it (#3373). */
22
+ parse(content: string, context?: ParseContext): TemplateIR;
21
23
  }
24
+ /** A compose parse result as the core TemplateIR. */
25
+ export declare function dockerResultToTemplate({ entities, warnings }: ParseResult): TemplateIR;
22
26
  /**
23
27
  * TypeScriptGenerator adapter — converts a TemplateIR back into DockerIR[]
24
28
  * and delegates to DockerGenerator, returning a single generated file.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/import/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAc,MAAM,gCAAgC,CAAC;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAiD5F;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAYjC;CACF;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,EAAE,CAIxC;CACF"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/import/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAc,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC3G,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE5F,OAAO,KAAK,EAA6B,WAAW,EAAE,MAAM,UAAU,CAAC;AA+CvE;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,kGAAkG;IAClG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,UAAU,CAOzD;CACF;AAED,qDAAqD;AACrD,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,WAAW,GAAG,UAAU,CAMtF;AAED;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,aAAa,EAAE,CAIxC;CACF"}
@@ -59,20 +59,24 @@ function resourceToDockerIr(resource) {
59
59
  * entities to a TemplateIR.
60
60
  */
61
61
  export class DockerTemplateParser {
62
- parse(content) {
62
+ /** With `context.embedded`, a config's `content` another lexicon owns is handed to it (#3373). */
63
+ parse(content, context) {
63
64
  // A Dockerfile has no name of its own; it imports as `app`
64
65
  // (Dockerfile.app). Rename the export to choose the file name.
65
66
  if (looksLikeDockerfile(content)) {
66
67
  return { resources: [dockerIrToResource(new DockerfileParser().parse("app", content))], parameters: [] };
67
68
  }
68
- const { entities, warnings } = new DockerParser().parse(content);
69
- return {
70
- resources: entities.map(dockerIrToResource),
71
- parameters: [],
72
- ...(warnings.length > 0 ? { warnings } : {}),
73
- };
69
+ return dockerResultToTemplate(new DockerParser().parse(content, context));
74
70
  }
75
71
  }
72
+ /** A compose parse result as the core TemplateIR. */
73
+ export function dockerResultToTemplate({ entities, warnings }) {
74
+ return {
75
+ resources: entities.map(dockerIrToResource),
76
+ parameters: [],
77
+ ...(warnings.length > 0 ? { warnings } : {}),
78
+ };
79
+ }
76
80
  /**
77
81
  * TypeScriptGenerator adapter — converts a TemplateIR back into DockerIR[]
78
82
  * and delegates to DockerGenerator, returning a single generated file.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where a Compose file embeds another lexicon's content, for `chant import`
3
+ * (#3373, #4388).
4
+ *
5
+ * A top-level config with `content` is a file Compose writes into each
6
+ * container that mounts it. The parser offers that text to core's
7
+ * embedded-content resolver, as k8s offers a ConfigMap value (#2962) and fly
8
+ * a machine file. `DockerOtelCollector` carries its collector config this way,
9
+ * at the otel lexicon's `COLLECTOR_CONFIG_PATH`, so a file or live import of
10
+ * the project gives the same otel source `chant import config.yaml` gives.
11
+ *
12
+ * Which lexicon imports the content is decided by core at run time; the hint
13
+ * below only names the owner a warning suggests when none is installed.
14
+ */
15
+ import type { EmbeddedContentResolver } from "@intentius/chant/import/embedded";
16
+ /**
17
+ * Offer each top-level config's `content` to `embedded`, replacing the value
18
+ * an owner imports with its reference. `configs` maps config name to its IR
19
+ * properties and is changed in place with fresh copies; `services` is the
20
+ * compose document's services section, read for where each config is mounted.
21
+ *
22
+ * The owner's modules go in a directory named after the one service that
23
+ * mounts the config, else after the config, so a file import and a live
24
+ * import (which cannot recover the config's key) write them to the same place.
25
+ */
26
+ export declare function delegateEmbedded(configs: Map<string, Record<string, unknown>>, services: Record<string, unknown> | undefined, embedded: EmbeddedContentResolver): void;
27
+ //# sourceMappingURL=embedded.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedded.d.ts","sourceRoot":"","sources":["../../src/import/embedded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAmB,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAiCjG;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC7C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC7C,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAoBN"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Where a Compose file embeds another lexicon's content, for `chant import`
3
+ * (#3373, #4388).
4
+ *
5
+ * A top-level config with `content` is a file Compose writes into each
6
+ * container that mounts it. The parser offers that text to core's
7
+ * embedded-content resolver, as k8s offers a ConfigMap value (#2962) and fly
8
+ * a machine file. `DockerOtelCollector` carries its collector config this way,
9
+ * at the otel lexicon's `COLLECTOR_CONFIG_PATH`, so a file or live import of
10
+ * the project gives the same otel source `chant import config.yaml` gives.
11
+ *
12
+ * Which lexicon imports the content is decided by core at run time; the hint
13
+ * below only names the owner a warning suggests when none is installed.
14
+ */
15
+ import { embeddedDocument } from "@intentius/chant/import/embedded";
16
+ import { COLLECTOR_CONFIG_PATH } from "@intentius/chant-lexicon-otel";
17
+ function isRecord(v) {
18
+ return typeof v === "object" && v !== null && !Array.isArray(v);
19
+ }
20
+ const COLLECTOR = { lexicon: "otel", what: "an OpenTelemetry Collector config" };
21
+ function hintFor(targets, doc) {
22
+ if (targets.includes(COLLECTOR_CONFIG_PATH))
23
+ return COLLECTOR;
24
+ if (isRecord(doc) && isRecord(doc.service) && isRecord(doc.service.pipelines))
25
+ return COLLECTOR;
26
+ return undefined;
27
+ }
28
+ /** Each service's mounts of top-level configs: config name, then the services and targets that mount it. */
29
+ function mountsByConfig(services) {
30
+ const out = new Map();
31
+ for (const [service, raw] of Object.entries(services ?? {})) {
32
+ if (!isRecord(raw) || !Array.isArray(raw.configs))
33
+ continue;
34
+ for (const c of raw.configs) {
35
+ const source = typeof c === "string" ? c : isRecord(c) && typeof c.source === "string" ? c.source : undefined;
36
+ if (!source)
37
+ continue;
38
+ const target = isRecord(c) && typeof c.target === "string" ? c.target : `/${source}`;
39
+ const list = out.get(source) ?? [];
40
+ list.push({ service, target });
41
+ out.set(source, list);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ /**
47
+ * Offer each top-level config's `content` to `embedded`, replacing the value
48
+ * an owner imports with its reference. `configs` maps config name to its IR
49
+ * properties and is changed in place with fresh copies; `services` is the
50
+ * compose document's services section, read for where each config is mounted.
51
+ *
52
+ * The owner's modules go in a directory named after the one service that
53
+ * mounts the config, else after the config, so a file import and a live
54
+ * import (which cannot recover the config's key) write them to the same place.
55
+ */
56
+ export function delegateEmbedded(configs, services, embedded) {
57
+ const mounts = mountsByConfig(services);
58
+ for (const [name, props] of configs) {
59
+ const text = props.content;
60
+ if (typeof text !== "string")
61
+ continue;
62
+ const doc = embeddedDocument(text);
63
+ if (!isRecord(doc))
64
+ continue;
65
+ const users = mounts.get(name) ?? [];
66
+ const owners = new Set(users.map((u) => u.service));
67
+ const ref = embedded.resolve({
68
+ host: "docker",
69
+ hostType: "config",
70
+ location: `config ${name} content`,
71
+ directory: owners.size === 1 ? [...owners][0] : name,
72
+ text,
73
+ document: doc,
74
+ expectedOwner: hintFor(users.map((u) => u.target), doc),
75
+ });
76
+ if (ref)
77
+ configs.set(name, { ...props, content: ref });
78
+ }
79
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A read-only Docker Engine API client for live export (#4388), and the tar
3
+ * reader for `GET /containers/{id}/archive`.
4
+ *
5
+ * The client is a single function, `(path) => { status, body }`, so tests hand
6
+ * export a fake Engine and never need a daemon. The default one speaks HTTP
7
+ * over the socket `DOCKER_HOST` names (`unix://…`, or plain `tcp://host:port`),
8
+ * else `/var/run/docker.sock`, with the unversioned API paths the daemon
9
+ * serves at its own version.
10
+ */
11
+ /** One GET against the Engine API. `path` starts with `/`. */
12
+ export type DockerEngine = (path: string, signal?: AbortSignal) => Promise<{
13
+ status: number;
14
+ body: Buffer;
15
+ }>;
16
+ /** Where `DOCKER_HOST` points: a unix socket path, or a host and port. */
17
+ export declare function engineAddress(dockerHost?: string | undefined): {
18
+ socketPath: string;
19
+ } | {
20
+ host: string;
21
+ port: number;
22
+ };
23
+ /** The Engine at `DOCKER_HOST` (or the default socket). */
24
+ export declare function defaultDockerEngine(dockerHost?: string): DockerEngine;
25
+ /** One entry of a tar stream. `type` is the ustar typeflag (`0` file, `5` directory, …). */
26
+ export interface TarEntry {
27
+ name: string;
28
+ type: string;
29
+ data: Buffer;
30
+ }
31
+ /** The entries of an uncompressed tar stream. PAX and GNU long-name records are applied to the entry after them. */
32
+ export declare function readTar(tar: Buffer): TarEntry[];
33
+ /**
34
+ * The text of the one regular file an archive holds, or undefined when it
35
+ * holds anything else (a directory, a link, several entries) or the bytes
36
+ * are not UTF-8 text.
37
+ */
38
+ export declare function singleFileText(tar: Buffer): string | undefined;
39
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/import/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAI7G,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,UAAU,qBAA0B,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAW3H;AAED,2DAA2D;AAC3D,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,YAAY,CAarE;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAQD,oHAAoH;AACpH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CA+B/C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A read-only Docker Engine API client for live export (#4388), and the tar
3
+ * reader for `GET /containers/{id}/archive`.
4
+ *
5
+ * The client is a single function, `(path) => { status, body }`, so tests hand
6
+ * export a fake Engine and never need a daemon. The default one speaks HTTP
7
+ * over the socket `DOCKER_HOST` names (`unix://…`, or plain `tcp://host:port`),
8
+ * else `/var/run/docker.sock`, with the unversioned API paths the daemon
9
+ * serves at its own version.
10
+ */
11
+ import { request } from "node:http";
12
+ const DEFAULT_SOCKET = "/var/run/docker.sock";
13
+ /** Where `DOCKER_HOST` points: a unix socket path, or a host and port. */
14
+ export function engineAddress(dockerHost = process.env.DOCKER_HOST) {
15
+ if (!dockerHost)
16
+ return { socketPath: DEFAULT_SOCKET };
17
+ if (dockerHost.startsWith("unix://"))
18
+ return { socketPath: dockerHost.slice("unix://".length) };
19
+ if (dockerHost.startsWith("tcp://")) {
20
+ if (process.env.DOCKER_TLS_VERIFY) {
21
+ throw new Error("docker live export: DOCKER_TLS_VERIFY is set, and a TLS Engine endpoint is not supported; use a unix socket or a plain tcp:// endpoint");
22
+ }
23
+ const url = new URL(`http://${dockerHost.slice("tcp://".length)}`);
24
+ return { host: url.hostname, port: Number(url.port || 2375) };
25
+ }
26
+ throw new Error(`docker live export: DOCKER_HOST ${dockerHost} is not a unix:// or tcp:// endpoint`);
27
+ }
28
+ /** The Engine at `DOCKER_HOST` (or the default socket). */
29
+ export function defaultDockerEngine(dockerHost) {
30
+ const address = engineAddress(dockerHost);
31
+ return (path, signal) => new Promise((resolve, reject) => {
32
+ const req = request({ ...address, path, method: "GET", headers: { Host: "docker" }, signal }, (res) => {
33
+ const chunks = [];
34
+ res.on("data", (c) => chunks.push(c));
35
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks) }));
36
+ res.on("error", reject);
37
+ });
38
+ req.on("error", reject);
39
+ req.end();
40
+ });
41
+ }
42
+ function field(block, start, length) {
43
+ const raw = block.subarray(start, start + length);
44
+ const end = raw.indexOf(0);
45
+ return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
46
+ }
47
+ /** The entries of an uncompressed tar stream. PAX and GNU long-name records are applied to the entry after them. */
48
+ export function readTar(tar) {
49
+ const out = [];
50
+ let offset = 0;
51
+ let longName;
52
+ while (offset + 512 <= tar.length) {
53
+ const header = tar.subarray(offset, offset + 512);
54
+ if (header.every((b) => b === 0))
55
+ break;
56
+ const size = parseInt(field(header, 124, 12).trim() || "0", 8);
57
+ const type = field(header, 156, 1) || "0";
58
+ const prefix = field(header, 345, 155);
59
+ let name = prefix ? `${prefix}/${field(header, 0, 100)}` : field(header, 0, 100);
60
+ const data = tar.subarray(offset + 512, offset + 512 + size);
61
+ offset += 512 + Math.ceil(size / 512) * 512;
62
+ if (type === "x") {
63
+ const path = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(data.toString("utf8"));
64
+ if (path)
65
+ longName = path[1];
66
+ continue;
67
+ }
68
+ if (type === "L") {
69
+ longName = data.toString("utf8").replace(/\0+$/, "");
70
+ continue;
71
+ }
72
+ if (type === "g")
73
+ continue;
74
+ if (longName !== undefined) {
75
+ name = longName;
76
+ longName = undefined;
77
+ }
78
+ out.push({ name, type: type === "\0" ? "0" : type, data: Buffer.from(data) });
79
+ }
80
+ return out;
81
+ }
82
+ /**
83
+ * The text of the one regular file an archive holds, or undefined when it
84
+ * holds anything else (a directory, a link, several entries) or the bytes
85
+ * are not UTF-8 text.
86
+ */
87
+ export function singleFileText(tar) {
88
+ const entries = readTar(tar);
89
+ if (entries.length !== 1 || entries[0].type !== "0")
90
+ return undefined;
91
+ const bytes = entries[0].data;
92
+ const text = bytes.toString("utf8");
93
+ return Buffer.from(text, "utf8").equals(bytes) ? text : undefined;
94
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/import/generator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAoE,MAAM,UAAU,CAAC;AAE3G,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,cAAc,CA+B7C;CACF"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/import/generator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAoE,MAAM,UAAU,CAAC;AAE3G,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,cAAc,CAoC7C;CACF"}
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Converts parsed IR to valid chant TypeScript source code.
5
5
  */
6
+ import { EmbeddedImportScope, isEmbeddedReference, renderEmbeddedReference } from "@intentius/chant/import/embedded";
6
7
  /**
7
8
  * Generator for Docker entities — IR → TypeScript chant source.
8
9
  */
@@ -12,16 +13,20 @@ export class DockerGenerator {
12
13
  const imports = new Set();
13
14
  const lines = [];
14
15
  const names = new ExportNames();
15
- for (const entity of entities) {
16
+ // Export names first, so the imports an embedded reference needs (#3373)
17
+ // never take one.
18
+ const exportNames = entities.map((e) => (e.kind === "dockerfile" ? sanitizeName(e.name) : names.take(e.name, e.kind)));
19
+ const scope = new EmbeddedImportScope("", [...RESERVED, ...exportNames]);
20
+ entities.forEach((entity, i) => {
16
21
  if (entity.kind === "dockerfile") {
17
22
  imports.add("Dockerfile");
18
23
  lines.push(generateDockerfile(entity, warnings));
19
- continue;
24
+ return;
20
25
  }
21
26
  const ctor = CONSTRUCTORS[entity.kind];
22
27
  imports.add(ctor);
23
- const exportName = names.take(entity.name, entity.kind);
24
- const expr = `new ${ctor}(${propsLiteral(entity)})`;
28
+ const exportName = exportNames[i];
29
+ const expr = `new ${ctor}(${propsLiteral(entity, scope)})`;
25
30
  // An export name that differs from the compose key would rename the
26
31
  // key on build, and every reference to it by name would dangle, so
27
32
  // the key is set explicitly (#3937).
@@ -32,9 +37,9 @@ export class DockerGenerator {
32
37
  imports.add("composeKey");
33
38
  lines.push(`export const ${exportName} = composeKey(${JSON.stringify(entity.name)}, ${expr});`);
34
39
  }
35
- }
40
+ });
36
41
  const importLine = `import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-docker";`;
37
- const source = [importLine, "", ...lines].join("\n");
42
+ const source = [importLine, ...scope.lines(), "", ...lines].join("\n");
38
43
  return { source, warnings };
39
44
  }
40
45
  }
@@ -45,11 +50,37 @@ const CONSTRUCTORS = {
45
50
  config: "DockerConfig",
46
51
  secret: "DockerSecret",
47
52
  };
48
- function propsLiteral(entity) {
49
- if (entity.kind === "service") {
50
- return JSON.stringify(entity.props, null, 2).replace(/"([a-z_][a-z0-9_]*)":/g, "$1:");
53
+ function propsLiteral(entity, scope) {
54
+ // Embedded references (a config's `content` another lexicon imported) are
55
+ // rendered as expressions in place of a placeholder string.
56
+ const refs = [];
57
+ const props = withPlaceholders(entity.props, refs);
58
+ let out;
59
+ if (entity.kind === "service" || refs.length > 0) {
60
+ out = JSON.stringify(props, null, 2).replace(/"([a-z_][a-z0-9_]*)":/g, "$1:");
61
+ }
62
+ else {
63
+ out = Object.keys(props).length > 0 ? JSON.stringify(props) : "{}";
64
+ }
65
+ refs.forEach((ref, i) => {
66
+ out = out.replace(JSON.stringify(placeholder(i)), () => renderEmbeddedReference(ref, scope, 2));
67
+ });
68
+ return out;
69
+ }
70
+ function placeholder(i) {
71
+ return `\u0000chant-embedded-${i}\u0000`;
72
+ }
73
+ function withPlaceholders(value, refs) {
74
+ if (isEmbeddedReference(value)) {
75
+ refs.push(value);
76
+ return placeholder(refs.length - 1);
77
+ }
78
+ if (Array.isArray(value))
79
+ return value.map((v) => withPlaceholders(v, refs));
80
+ if (value && typeof value === "object") {
81
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withPlaceholders(v, refs)]));
51
82
  }
52
- return Object.keys(entity.props).length > 0 ? JSON.stringify(entity.props) : "{}";
83
+ return value;
53
84
  }
54
85
  /** Words an export cannot be named, and the names the generated file imports. */
55
86
  const RESERVED = new Set([
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Pure export helpers for the docker lexicon (#4388): turn what the Engine API
3
+ * returns for a compose project's containers back into the compose document a
4
+ * user would have written, and map it to import IR through `DockerParser`. All
5
+ * Engine I/O stays in the caller (`../export-resources`).
6
+ *
7
+ * A compose project is the set of containers labelled
8
+ * `com.docker.compose.project=<name>`; each service is the container labelled
9
+ * `com.docker.compose.service`, the lowest `container-number` when scaled.
10
+ * One-off containers (`compose run`) are skipped.
11
+ *
12
+ * Configs. Outside swarm, a top-level config is not an Engine object. A
13
+ * `content` config is copied into the container when it is created, and a
14
+ * `file` config is bind-mounted; neither is named anywhere the Engine keeps.
15
+ * What the container does keep is its command, so the files a service is
16
+ * told to read (`--config=/etc/otelcol-contrib/config.yaml`) are the
17
+ * candidates, and the caller reads each through
18
+ * `GET /containers/{id}/archive?path=…`, which works for both forms and for a
19
+ * remote Engine whose host paths are out of reach. A candidate that reads back
20
+ * as one text file becomes a `content` config mounted at that path, named
21
+ * `<service>-<file name>` (the original key is not recoverable). A candidate
22
+ * inside a volume or a directory bind mount belongs to that mount and is not
23
+ * read.
24
+ *
25
+ * The strip. By default fields the image supplies (its command, entrypoint,
26
+ * environment, labels, working dir and user) are dropped, so a service reaches
27
+ * the shape it was declared in; `verbatim` keeps them. Compose's own
28
+ * `com.docker.compose.*` labels are always dropped: compose writes them, and a
29
+ * declaration that set them would fight it.
30
+ *
31
+ * Ownership. The docker serializer stamps no marker, so under `owned` only
32
+ * services carrying the label marker (`app.kubernetes.io/managed-by: chant`,
33
+ * set with `defaultLabels`) are exported, and the caller logs that once.
34
+ */
35
+ import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
36
+ export declare const PROJECT_LABEL = "com.docker.compose.project";
37
+ export declare const SERVICE_LABEL = "com.docker.compose.service";
38
+ type Json = Record<string, unknown>;
39
+ /** A container's or an image's healthcheck, durations in nanoseconds. */
40
+ export interface HealthcheckInspect {
41
+ Test?: string[];
42
+ Interval?: number;
43
+ Timeout?: number;
44
+ Retries?: number;
45
+ StartPeriod?: number;
46
+ }
47
+ /** What `GET /containers/{id}/json` returns, as far as export reads it. */
48
+ export interface ContainerInspect {
49
+ Id?: string;
50
+ Name?: string;
51
+ Config?: {
52
+ Image?: string;
53
+ Cmd?: string[] | null;
54
+ Entrypoint?: string[] | null;
55
+ Env?: string[] | null;
56
+ Labels?: Record<string, string> | null;
57
+ WorkingDir?: string;
58
+ User?: string;
59
+ Healthcheck?: HealthcheckInspect | null;
60
+ };
61
+ HostConfig?: {
62
+ PortBindings?: Record<string, Array<{
63
+ HostIp?: string;
64
+ HostPort?: string;
65
+ }> | null> | null;
66
+ RestartPolicy?: {
67
+ Name?: string;
68
+ MaximumRetryCount?: number;
69
+ } | null;
70
+ };
71
+ Mounts?: Array<{
72
+ Type?: string;
73
+ Name?: string;
74
+ Source?: string;
75
+ Destination?: string;
76
+ RW?: boolean;
77
+ }>;
78
+ NetworkSettings?: {
79
+ Networks?: Record<string, unknown> | null;
80
+ };
81
+ }
82
+ /** What `GET /images/{name}/json` returns, as far as export reads it. */
83
+ export interface ImageInspect {
84
+ Config?: {
85
+ Cmd?: string[] | null;
86
+ Entrypoint?: string[] | null;
87
+ Env?: string[] | null;
88
+ Labels?: Record<string, string> | null;
89
+ WorkingDir?: string;
90
+ User?: string;
91
+ Healthcheck?: HealthcheckInspect | null;
92
+ };
93
+ }
94
+ /** One service as read from the Engine: its container, its image, and the config files read out of it. */
95
+ export interface LiveService {
96
+ name: string;
97
+ container: ContainerInspect;
98
+ image?: ImageInspect;
99
+ /** Config target path → the text read from the container. */
100
+ files: Record<string, string>;
101
+ }
102
+ /**
103
+ * The containers to export, one per service: compose-labelled for `project`,
104
+ * not one-off, and the lowest container number when a service is scaled.
105
+ * `summaries` is what `GET /containers/json` returns.
106
+ */
107
+ export declare function serviceContainers(project: string, summaries: Array<{
108
+ Id?: string;
109
+ Labels?: Record<string, string> | null;
110
+ }>): Map<string, string>;
111
+ /**
112
+ * The paths a container's command names that may be config files: an
113
+ * absolute path with a file extension, alone or as a `--flag=` value (an
114
+ * otel `file:` URI included). A path inside a volume, or inside a directory
115
+ * bind mount, belongs to that mount and is left out.
116
+ */
117
+ export declare function configCandidates(container: ContainerInspect): string[];
118
+ /**
119
+ * The compose document a project's services were created from. External
120
+ * volumes and networks (not prefixed with the project name) are declared
121
+ * `external`.
122
+ */
123
+ export declare function composeFromLive(project: string, services: LiveService[], opts?: {
124
+ verbatim?: boolean;
125
+ }): Json;
126
+ /** Whether a container carries chant's label marker. */
127
+ export declare function isChantOwned(container: ContainerInspect): boolean;
128
+ /**
129
+ * Build export IR from one compose project's services, through
130
+ * {@link DockerParser} as the single compose → IR mapping. `owned` keeps only
131
+ * services carrying chant's marker; `selector` narrows by IR type (`service`,
132
+ * `config`, …) and compose key. `reparse(context)` parses the same document
133
+ * again with a `ParseContext`, so `chant import --from` hands a collector
134
+ * config to the otel importer, as a file import does (#3373).
135
+ */
136
+ export declare function buildExportFromProject(project: string, services: LiveService[], opts?: {
137
+ verbatim?: boolean;
138
+ selector?: ResourceSelector;
139
+ owned?: boolean;
140
+ }): ExportedTemplate;
141
+ export {};
142
+ //# sourceMappingURL=live-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-export.d.ts","sourceRoot":"","sources":["../../src/import/live-export.ts"],"names":[],"mappings":"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"}