@spunto/build 0.1.0 → 0.2.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/README.md CHANGED
@@ -53,6 +53,28 @@ const forCodeServer = codeServerGallery(rawBlob) // the same choice, handed to t
53
53
  environment variable — and every other function takes its result. Nothing is memoized at import
54
54
  time, so one process can serve several tenants on several registries.
55
55
 
56
+ ### `@spunto/build/naming` — the names both sides have to pronounce the same way
57
+
58
+ A worker is a container called `mp-worker-<id>`, with volumes and a network named after it, running
59
+ an image called `mp-proj-<slug>:v<n>`. None of that is an implementation detail — each name is a
60
+ contract between the script that writes it, the Docker client that looks it up and the UI that
61
+ shows it:
62
+
63
+ ```ts
64
+ import { workerContainerName, workerVolumePrefix, projectImageRef } from "@spunto/build/naming"
65
+
66
+ docker.getContainer(workerContainerName(workerId))
67
+ const ref = projectImageRef(projectId, version) // mp-proj-my-app:v7
68
+ ```
69
+
70
+ Also here: `workerNetworkName`, `workerVolumeName`, `isProjectImageRef`, `SHARED_NETWORK`, and the
71
+ paths a worker and its control plane agree on — `WORKER_STATUS_FILE` / `workerStatusPath`,
72
+ `EXTENSIONS_DIR`, `FEATURE_ENTRYPOINTS_DIR`, `EXTENSION_FAILURES_FILE`.
73
+
74
+ **These are formats, not preferences.** Changing a value renames things that already exist on
75
+ running machines: a live worker's container and volumes keep the name they were created with. Read
76
+ a diff in that file as a migration, not a rename.
77
+
56
78
  ## The rule
57
79
 
58
80
  A module belongs in this package only if it imports **no** ORM schema, **no** HTTP framework, **no**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/build",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Spunto's shared Build engine \u2014 the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,10 @@
34
34
  "./extensions": {
35
35
  "types": "./src/extensions/index.ts",
36
36
  "import": "./src/extensions/index.ts"
37
+ },
38
+ "./naming": {
39
+ "types": "./src/naming/index.ts",
40
+ "import": "./src/naming/index.ts"
37
41
  }
38
42
  },
39
43
  "files": [
package/src/index.ts CHANGED
@@ -7,3 +7,4 @@
7
7
 
8
8
  export * from "./steps/index"
9
9
  export * from "./extensions/index"
10
+ export * from "./naming/index"
@@ -0,0 +1,105 @@
1
+ // The names both control planes have to pronounce the same way.
2
+ //
3
+ // A worker is a container called `mp-worker-<id>`, with volumes named after it, on a network named
4
+ // after it, running an image called `mp-proj-<slug>:v<n>`, writing its progress to a file at a
5
+ // fixed path. None of that is an implementation detail: each name is a contract between the shell
6
+ // script that writes it, the Docker client that looks it up, and the UI that shows it.
7
+ //
8
+ // Until now none of them had a single definition. `mp-worker-<id>` was rebuilt by hand in fourteen
9
+ // places across two apps — and a typo in one of them doesn't fail to compile, it produces a
10
+ // container nobody can find at runtime. Spunto Lite rebuilds the same names again, on its side.
11
+ //
12
+ // **These are formats, not preferences.** Changing a value here renames things that already exist
13
+ // on real machines: a running worker's container and volumes keep the name they were created with.
14
+ // Treat every constant below as versioned protocol, the same way `../steps` treats its markers.
15
+ //
16
+ // The `mp-` prefix is historical (the product was called something else) and is deliberately left
17
+ // alone: it is baked into containers, volumes and images that are running right now.
18
+
19
+ /** Container that *is* the worker. */
20
+ export function workerContainerName(workerId: string): string {
21
+ return `mp-worker-${workerId}`
22
+ }
23
+
24
+ /**
25
+ * Per-worker bridge network. Isolates each worker at L3 so no worker can reach another worker's
26
+ * container IP directly, bypassing the proxy.
27
+ */
28
+ export function workerNetworkName(workerId: string): string {
29
+ return `mp-worker-${workerId}-net`
30
+ }
31
+
32
+ /** The named volumes a worker owns, and what each holds. */
33
+ export const WORKER_VOLUME_KINDS = ["workspace", "home", "docker", "containerd"] as const
34
+ export type WorkerVolumeKind = (typeof WORKER_VOLUME_KINDS)[number]
35
+
36
+ /** One of a worker's named volumes — `workspace` for the code, `docker`/`containerd` for DinD. */
37
+ export function workerVolumeName(workerId: string, kind: WorkerVolumeKind): string {
38
+ return `mp-worker-${workerId}-${kind}`
39
+ }
40
+
41
+ /**
42
+ * Prefix every volume of one worker shares. Teardown and disk accounting both scan by prefix rather
43
+ * than by listing kinds, so that a volume added later is still found by code written before it.
44
+ */
45
+ export function workerVolumePrefix(workerId: string): string {
46
+ return `mp-worker-${workerId}-`
47
+ }
48
+
49
+ /**
50
+ * Image a project's workers boot from, keyed by the project's *version* — see the image cache in
51
+ * docs/architecture-deep-dive.md. The slug is derived, not stored: an image ref may only hold
52
+ * lowercase alphanumerics and separators, and a project id is not guaranteed to.
53
+ */
54
+ export function projectImageRef(projectId: string, version: number): string {
55
+ const slug = projectId
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9]/g, "-")
58
+ .replace(/-+/g, "-")
59
+ .replace(/^-|-$/g, "")
60
+ return `mp-proj-${slug}:v${version}`
61
+ }
62
+
63
+ /** True for an image this platform built, as opposed to a base image it pulled. */
64
+ export function isProjectImageRef(ref: string): boolean {
65
+ return /^mp-proj-[a-z0-9-]+:v\d+$/.test(ref)
66
+ }
67
+
68
+ /**
69
+ * Network every shared service joins under its slug, and every worker joins at spawn, so a
70
+ * workspace reaches `http://<slug>:<port>` by DNS. Spunto Lite's "shared services"; the local
71
+ * counterpart of the Ship pillar.
72
+ */
73
+ export const SHARED_NETWORK = "mp-shared-net"
74
+
75
+ /**
76
+ * Where a worker reports its own setup progress, read back by the control plane.
77
+ *
78
+ * Relative to the user's home rather than absolute: the remote user is a project setting, so the
79
+ * absolute path is only known once you know who the worker runs as — hence `workerStatusPath`.
80
+ */
81
+ export const WORKER_STATUS_FILE = ".mp-status.json"
82
+
83
+ /** Absolute path of the status file for a given home directory, e.g. `/home/vscode`. */
84
+ export function workerStatusPath(home: string): string {
85
+ return `${home.replace(/\/+$/, "")}/${WORKER_STATUS_FILE}`
86
+ }
87
+
88
+ /**
89
+ * VS Code extensions are installed into the *image*, at a system path — not into a home, which is
90
+ * per worker and would make every worker re-download them.
91
+ */
92
+ export const EXTENSIONS_DIR = "/opt/mp-extensions"
93
+
94
+ /**
95
+ * Directory of executables a devcontainer feature can drop to have them run at container start.
96
+ * The devcontainer `entrypoint` contract, implemented on our side.
97
+ */
98
+ export const FEATURE_ENTRYPOINTS_DIR = "/etc/mp-feature-entrypoints"
99
+
100
+ /**
101
+ * Extensions the image build could not install, one id per line. Left in the image so a worker can
102
+ * tell what is missing without anyone re-reading the build log — see `EXTENSION_FAILED_MARKER` in
103
+ * `../steps`, which is the same information on its way out.
104
+ */
105
+ export const EXTENSION_FAILURES_FILE = "/etc/mp-extension-failures"