@jr2/orchestrator 0.2.1 → 0.4.1
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/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +10 -1
- package/console/components/fleet.ts +39 -6
- package/console/components/machine-pane.ts +89 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +110 -39
- package/console/store.ts +221 -17
- package/console/style.css +90 -0
- package/package.json +4 -5
- package/src/actor.ts +507 -54
- package/src/agent.ts +2 -2
- package/src/ambient.ts +2 -0
- package/src/assembly-checks.ts +165 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/chart.ts +2050 -0
- package/src/charts.ts +720 -0
- package/src/config.ts +233 -22
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +313 -0
- package/src/custodian.ts +951 -0
- package/src/customize.ts +216 -25
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +905 -0
- package/src/effector-wire.ts +525 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +247 -25
- package/src/held-secrets.ts +643 -0
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +514 -119
- package/src/images.ts +58 -31
- package/src/index.ts +49 -4
- package/src/instance.ts +445 -59
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/model-keys.ts +92 -0
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +89 -7
- package/src/notices.ts +88 -0
- package/src/open-body.ts +1444 -0
- package/src/open-effect.ts +144 -0
- package/src/open.ts +4 -4
- package/src/parts.ts +530 -33
- package/src/party.ts +468 -0
- package/src/pick.ts +309 -0
- package/src/pool.ts +1 -1
- package/src/registration.ts +301 -17
- package/src/repo-fetch.ts +61 -86
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1841 -105
- package/src/sandbox-effect.ts +891 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1719 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +494 -0
- package/src/server.ts +285 -89
- package/src/setup.ts +589 -70
- package/src/size.ts +342 -0
- package/src/snapshot-store.ts +64 -13
- package/src/stub-harness.ts +5 -0
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +178 -0
- package/src/tokens.ts +44 -12
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- package/src/sandbox-kubectl.ts +0 -1144
package/src/server.ts
CHANGED
|
@@ -1,149 +1,288 @@
|
|
|
1
|
-
// The deployed orchestrator's entrypoint (ADR-0019): the process the
|
|
2
|
-
// the same token, the e2e tier's per-scenario fixture (ADR-0010 boots exactly this
|
|
3
|
-
// host). Configuration is env, kube-style; the instance folder is the process cwd
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The deployed orchestrator's entrypoint (ADR-0019): the process the Orchestrator Kit image runs
|
|
2
|
+
// — and, by the same token, the e2e tier's per-scenario fixture (ADR-0010 boots exactly this
|
|
3
|
+
// process on the host). Configuration is env, kube-style; the instance folder is the process cwd.
|
|
4
|
+
// The Kit image holds the engine there and nothing of any Instance (ADR-0069): the Workflows and
|
|
5
|
+
// `jr2.config.ts` are the CURRENT ASSEMBLY's, which `jr2 up` publishes onto the state volume
|
|
6
|
+
// (assembly.ts). The folder the process starts in is never loaded: a host-booted fixture publishes
|
|
7
|
+
// through the same door. No dev.json, no stub Harness: those were host-dev affordances, and there
|
|
8
|
+
// is no host dev mode.
|
|
6
9
|
//
|
|
7
10
|
// PORT listen port (default 4000 — what the Service targets)
|
|
8
11
|
// HOST listen hostname (default 0.0.0.0: pods must be reachable off-loopback)
|
|
9
12
|
// JR2_INSTANCE_TOKEN the Instance credential (ADR-0013), from the instance's Secret; minted
|
|
10
13
|
// per boot when absent (then only the announce line knows it — fixtures set it)
|
|
14
|
+
// JR2_PUBLISH_TOKEN the publish credential (ADR-0069): `PUT /assembly` alone accepts it. From
|
|
15
|
+
// the same Secret; minted per boot when absent, so no host-booted process has
|
|
16
|
+
// a publish door anyone outside it can open.
|
|
11
17
|
// JR2_SIGNING_KEY base64 key Sandbox tokens are signed with; from the Secret so live Sandboxes
|
|
12
18
|
// survive a pod restart. Absent → minted into `<dir>/.jr2/secret` (dev-grade).
|
|
13
19
|
// JR2_NAMESPACE the pod's own namespace (Deployment fieldRef) — presence = "deployed":
|
|
14
|
-
// Sandboxes are driven in it, and the
|
|
15
|
-
// <git.credentials[].token>
|
|
16
|
-
// each token env var the config names rides the instance Secret (ADR-0051):
|
|
17
|
-
// the Orchestrator materializes it into the Repo's credential Secret.
|
|
20
|
+
// Sandboxes are driven in it, and the Custodians' route home is Service DNS.
|
|
18
21
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
22
|
+
// No git token reaches this process (ADR-0068): `jr2 up` writes each entry's clone Secret, and
|
|
23
|
+
// the Repo port names it from `git-clone.json` in the `jr2-held` mount.
|
|
24
|
+
//
|
|
25
|
+
// The first stdout line is one JSON object `{ url, workflows, assembly, kitVersion }` — the
|
|
26
|
+
// discovery seam a fixture (or a human tailing pod logs) parses instead of racing the socket.
|
|
27
|
+
// `assembly` is the current Assembly's hash, or `null` with `assemblyError` saying why when the
|
|
28
|
+
// volume named one this process cannot load. The line also names the runs the boot did not resume
|
|
29
|
+
// (`lost`, `drifted`, `failed`; ADR-0030), each restored inside the Assembly its snapshot names,
|
|
30
|
+
// and the Assemblies the boot `collected` because no live run holds them (ADR-0069), and, when any
|
|
31
|
+
// exists, the named Charts as `charts: { files, runtime, unrunnable }` (ADR-0070). A boot with
|
|
32
|
+
// no current Assembly restores nothing: it names the live runs it left untouched as `waiting`, and
|
|
33
|
+
// the first publish that lands restores them and announces one more line,
|
|
34
|
+
// `{ assembly, lost?, drifted?, failed? }`, when any run was not resumed. The hourly sweep
|
|
35
|
+
// announces what it collects as one more line, `{ collected }`; a publish names it in its answer.
|
|
36
|
+
// Deployed, the boot then creates one `Repo` resource per identity the Machines of its held
|
|
37
|
+
// Assemblies bind (ADR-0051, ADR-0069)
|
|
38
|
+
// and announces each as `{ repo, url, bound: true }` — or `{ repo, error }` — one line apiece,
|
|
39
|
+
// after serving: a Repo the cluster refuses is a degraded Repo (ADR-0048), never a boot that did
|
|
40
|
+
// not happen. An Assembly that binds none still runs the same pass and binds nothing, which
|
|
41
|
+
// unlabels every Repo a previous deploy bound — an Instance that drops its last `workspace()`
|
|
42
|
+
// leaves `jr2 gc` able to collect what it stopped using. Each publish runs the pass again. With no
|
|
43
|
+
// current Assembly there is no pass: nothing says what is bound, and the next publish decides.
|
|
27
44
|
|
|
28
45
|
import { join } from "node:path";
|
|
29
|
-
import {
|
|
46
|
+
import { resolveAssembly, type ResolvedAssembly } from "./assembly.ts";
|
|
47
|
+
import { KIT_VERSION, loadConfig, type JR2Config } from "./config.ts";
|
|
48
|
+
import { readImageRefs } from "./images.ts";
|
|
49
|
+
import { readMountDigests } from "./mount-digest.ts";
|
|
50
|
+
import { GIT_CLONE_MANIFEST_KEY, readGitCloneManifest } from "./git-clone.ts";
|
|
30
51
|
import { loadWorkflows, startInstance, type RunningInstance } from "./instance.ts";
|
|
31
52
|
import {
|
|
32
53
|
HARNESS_CONFIGMAP,
|
|
33
54
|
HARNESS_CONFIG_KEY,
|
|
34
55
|
HARNESS_ENV_SECRET,
|
|
56
|
+
HELD_MOUNT,
|
|
35
57
|
IMAGES_KEY,
|
|
36
58
|
IMAGES_MOUNT,
|
|
37
59
|
INSTANCE_HARNESS_PORT,
|
|
38
60
|
INSTANCE_HARNESS_SERVICE,
|
|
39
|
-
|
|
61
|
+
INSTANCE_EFFECTOR,
|
|
62
|
+
EFFECTOR_PORT,
|
|
40
63
|
} from "./names.ts";
|
|
64
|
+
import type { AnyStateMachine } from "xstate";
|
|
41
65
|
import { partsOf, type CarriedRepo } from "./parts.ts";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
66
|
+
import { kubeClient, type KubeClient } from "./kube-client.ts";
|
|
67
|
+
import { kubeRepoFetches, type RepoFetches } from "./repo-fetch.ts";
|
|
68
|
+
import { kubeRepos, type RepoResources } from "./repos.ts";
|
|
69
|
+
import { SANDBOX_EFFECTOR_PORT, kubeSandbox } from "./sandbox-kube.ts";
|
|
70
|
+
import { sandboxEffectorService } from "./sandbox-effector.ts";
|
|
71
|
+
import { watchSandboxes, type SandboxWatch } from "./sandbox-watch.ts";
|
|
45
72
|
import { loadSigningKey, mintInstanceToken } from "./tokens.ts";
|
|
46
73
|
import type { SandboxPort } from "./workspace.ts";
|
|
47
74
|
|
|
48
75
|
export type ServerMainOptions = {
|
|
49
|
-
/** The instance folder (deployed: the image's WORKDIR
|
|
76
|
+
/** The instance folder (deployed: the image's WORKDIR, with the state volume at `.jr2`;
|
|
77
|
+
* fixtures: a temp instance, whose `.jr2` receives what the fixture publishes). */
|
|
50
78
|
dir: string;
|
|
51
79
|
/** The environment to read the contract above from (deployed: `process.env`). */
|
|
52
80
|
env: Record<string, string | undefined>;
|
|
53
81
|
/** Where the one-line JSON announcement goes (deployed: stdout). */
|
|
54
82
|
announce: (line: string) => void;
|
|
55
|
-
/** The
|
|
56
|
-
* Deployed: the
|
|
57
|
-
|
|
83
|
+
/** The Kubernetes client behind the data plane's ports (ADR-0063), injectable for tests.
|
|
84
|
+
* Deployed: the in-cluster client over the Pod's ServiceAccount. */
|
|
85
|
+
kube?: KubeClient;
|
|
58
86
|
};
|
|
59
87
|
|
|
60
88
|
/** Boot the instance the env describes; resolves once serving (the caller owns signals/exit). */
|
|
61
89
|
export async function serverMain(opts: ServerMainOptions): Promise<RunningInstance> {
|
|
62
90
|
const { env } = opts;
|
|
63
|
-
const config = await loadConfig(opts.dir);
|
|
64
91
|
const port = env.PORT !== undefined ? Number(env.PORT) : 4000;
|
|
65
92
|
const signingKey =
|
|
66
93
|
env.JR2_SIGNING_KEY !== undefined ? Buffer.from(env.JR2_SIGNING_KEY, "base64") : await loadSigningKey(opts.dir);
|
|
67
94
|
|
|
68
|
-
// Deployed (JR2_NAMESPACE set):
|
|
69
|
-
// by nature, which is what lets live Sandboxes outlive orchestrator restarts (ADR-0013).
|
|
95
|
+
// Deployed (JR2_NAMESPACE set): Custodians dial the orchestrator at its own Service DNS — stable
|
|
96
|
+
// by nature, which is what lets live Sandboxes outlive orchestrator restarts (ADR-0013). `jr2 up`
|
|
97
|
+
// writes that address into the Custodian's bootstrap; nothing here passes it on.
|
|
70
98
|
const namespace = env.JR2_NAMESPACE;
|
|
71
99
|
|
|
72
100
|
// From the instance Secret when deployed; per-boot for a host-booted fixture.
|
|
73
101
|
const instanceToken = env.JR2_INSTANCE_TOKEN ?? mintInstanceToken();
|
|
102
|
+
const publishToken = env.JR2_PUBLISH_TOKEN ?? mintInstanceToken();
|
|
103
|
+
|
|
104
|
+
// The current Assembly (ADR-0069), and its config and Workflows read from its ROOT, never from
|
|
105
|
+
// the folder directly. A published Assembly that does not load (a Kit bump that broke an import,
|
|
106
|
+
// say) is no current Assembly: the process serves and says why (ADR-0048).
|
|
107
|
+
let resolved: ResolvedAssembly = await resolveAssembly(opts.dir);
|
|
108
|
+
// The config that applies is ALWAYS the current Assembly's (ADR-0069): the ports read it through
|
|
109
|
+
// this binding, and a publish replaces it. `bound` is whether a current Assembly binds one at all:
|
|
110
|
+
// an Assembly with no `jr2.config.ts` binds `undefined`, which is the defaults; no current
|
|
111
|
+
// Assembly binds nothing. In that window no run runs: the boot defers its restore to the first
|
|
112
|
+
// publish, which binds a config before it restores (instance.ts), and no Workflow is registered
|
|
113
|
+
// to start. The read routes (`GET /repos`, `GET /assembly`, `/healthz`, the feeds) read no
|
|
114
|
+
// config, so they answer in the window.
|
|
115
|
+
let config: JR2Config | undefined;
|
|
116
|
+
let bound = false;
|
|
117
|
+
// The current Assembly's hash, beside its config: a held run's provision names it (ADR-0069).
|
|
118
|
+
let currentHash: string | undefined;
|
|
119
|
+
let carried = partsOf([]);
|
|
120
|
+
if (resolved.current) {
|
|
121
|
+
const current = resolved.current;
|
|
122
|
+
try {
|
|
123
|
+
config = await loadConfig(current.root);
|
|
124
|
+
carried = partsOf((await loadWorkflows(current.root)).map((w) => w.machine));
|
|
125
|
+
bound = true;
|
|
126
|
+
currentHash = current.hash;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
config = undefined;
|
|
129
|
+
resolved = {
|
|
130
|
+
current: null,
|
|
131
|
+
error: `the current Assembly ${current.hash} does not load: ${err instanceof Error ? err.message : err}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
74
135
|
|
|
75
|
-
// The data
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
const carried = partsOf((await loadWorkflows(opts.dir)).map((w) => w.machine));
|
|
81
|
-
const dataPlane = carried.composesSandbox && namespace !== undefined;
|
|
136
|
+
// The data plane is ALWAYS wired when deployed (ADR-0069): a publish can bring the first
|
|
137
|
+
// `workspace()`, and wiring a client is free. `jr2 up` still decides from its host-side walk
|
|
138
|
+
// whether to DEPLOY the operator; a provision on a cluster with none fails naming `jr2 up`
|
|
139
|
+
// (sandbox-kube.ts). A host-booted process has no cluster: its `workspace()` runs fault pointedly.
|
|
140
|
+
const dataPlane = namespace !== undefined;
|
|
82
141
|
let sandbox: SandboxPort | undefined;
|
|
83
142
|
let repos: RepoResources | undefined;
|
|
84
143
|
let fetches: RepoFetches | undefined;
|
|
85
|
-
|
|
86
|
-
// writer that ever REMOVES `jr2.dev/bound` (repos.ts), and an Instance that drops its last
|
|
87
|
-
// `workspace()` still owns the Repos its earlier deploys bound. So the port is built whenever
|
|
88
|
-
// there is a cluster to drive, and the walk — now naming nothing — unlabels every one of them,
|
|
89
|
-
// which is what puts them on `jr2 gc`'s clock. Gate it on the data plane instead and they stay
|
|
90
|
-
// bound forever: uncollectable resources, with their node caches behind them.
|
|
144
|
+
let watch: LazyWatch | undefined;
|
|
91
145
|
if (namespace !== undefined) {
|
|
92
|
-
|
|
146
|
+
/** The config that applies now, or a pointed refusal while no current Assembly binds one.
|
|
147
|
+
* Only a provision (`settings`) and a Repo pass (`credentials`, through `bind`/`ensure`) read
|
|
148
|
+
* it; `repos.list()`, which `GET /repos` reads, does not. No run reaches it in the window: the
|
|
149
|
+
* restore is deferred and nothing is registered to start. The guard exists so that a future
|
|
150
|
+
* caller cannot provision with an empty fence and no placement, a config nobody wrote. */
|
|
151
|
+
const applied = (): JR2Config | undefined => {
|
|
152
|
+
if (!bound) throw new Error(NO_CONFIG);
|
|
153
|
+
return config;
|
|
154
|
+
};
|
|
155
|
+
const credentials = () => applied()?.git?.credentials ?? [];
|
|
156
|
+
// ONE client for the process (ADR-0063): one write cap over every port that writes, and the
|
|
157
|
+
// token read fresh on every request. Nothing is dialed until a port first asks.
|
|
158
|
+
const client = opts.kube ?? kubeClient({ env });
|
|
93
159
|
// The Repo resources (ADR-0051): created by this process, cloned by the operator's cache agent
|
|
94
160
|
// on every node that needs them. The port resolves `git.credentials` into each resource's
|
|
95
|
-
// `secretRef
|
|
96
|
-
// `
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
// `secretRef` and only names the Secret: `jr2 up` wrote each token's clone Secret and listed
|
|
162
|
+
// them in `git-clone.json`, read here per call so a converge reaches the next attach (ADR-0068).
|
|
163
|
+
// This port is also what unlabels the Repos an earlier deploy bound, so it hangs off DEPLOYED.
|
|
164
|
+
repos = kubeRepos({
|
|
165
|
+
namespace,
|
|
166
|
+
credentials,
|
|
167
|
+
cloneSecrets: async () => new Set((await readGitCloneManifest(join(HELD_MOUNT, GIT_CLONE_MANIFEST_KEY))).secrets),
|
|
168
|
+
client,
|
|
169
|
+
});
|
|
170
|
+
// The one watch (ADR-0063): every Sandbox of this Instance, shared by the provision, the lease
|
|
171
|
+
// and the fetch ask. Lazy, because a cluster where `jr2 up` deployed no operator serves no
|
|
172
|
+
// Sandbox kind, and a watch on it would only log. It starts at boot when the current Assembly
|
|
173
|
+
// composes a Sandbox: its first list IS the restore reconcile (ADR-0012).
|
|
174
|
+
watch = lazyWatch(() => watchSandboxes(client, { namespace }));
|
|
175
|
+
if (carried.composesSandbox) watch.start();
|
|
176
|
+
// The ask a pod makes when something inside it fetches (ADR-0053): it marks the Sandbox CR and
|
|
177
|
+
// waits on the same watch the provision waits on.
|
|
178
|
+
fetches = kubeRepoFetches({ namespace, client, watch });
|
|
179
|
+
sandbox = kubeSandbox({
|
|
180
|
+
// Named here the same way HARNESS_CONFIGMAP is: a jr2-owned mount path, deliberately NOT an
|
|
181
|
+
// env knob — there is no image escape hatch left to configure (ADR-0038). Note what this
|
|
182
|
+
// buys: the map is read per provision, so an instance whose `jr2-images` ConfigMap is not yet
|
|
183
|
+
// mounted still BOOTS and serves — only a provision fails, pointing at `jr2 up`. That is the
|
|
184
|
+
// correct blast pattern, and the stale-read window is one kubelet propagation.
|
|
185
|
+
imagesPath: join(IMAGES_MOUNT, IMAGES_KEY),
|
|
186
|
+
// What comes from the config, read per provision off the current Assembly's (ADR-0069).
|
|
187
|
+
settings: () => {
|
|
188
|
+
const config = applied();
|
|
189
|
+
return {
|
|
190
|
+
// The fence (ADR-0051): a per-run url must match one of these, or the provision refuses it.
|
|
191
|
+
credentials: credentials(),
|
|
192
|
+
// The Harness containers' env (ADR-0018): what this instance can REACH (the custom provider
|
|
193
|
+
// — no Agents, they ride each Turn since ADR-0049), then the instance's own valueFrom
|
|
194
|
+
// entries (literal values already live in the jr2-harness-env Secret below).
|
|
195
|
+
env: [
|
|
196
|
+
{
|
|
197
|
+
name: "JR2_HARNESS_JSON",
|
|
198
|
+
valueFrom: { configMapKeyRef: { name: HARNESS_CONFIGMAP, key: HARNESS_CONFIG_KEY } },
|
|
199
|
+
},
|
|
200
|
+
...(config?.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
|
|
201
|
+
],
|
|
202
|
+
envFrom: [{ secretRef: { name: HARNESS_ENV_SECRET } }, ...(config?.harness?.envFrom ?? [])],
|
|
203
|
+
// Presence only — the PEM itself was materialized into the jr2-ca ConfigMap by `jr2 up`
|
|
204
|
+
// (ADR-0020); the in-cluster config eval never reads the file.
|
|
205
|
+
caBundle: config?.harness?.caBundle !== undefined,
|
|
206
|
+
// Which nodes are Sandbox nodes (ADR-0052) — the CR carries it, the operator reads no config.
|
|
207
|
+
...(config?.sandbox ? { placement: config.sandbox } : {}),
|
|
208
|
+
// The Size of a Workspace that states none (ADR-0060), and the class its pod runs under.
|
|
209
|
+
...(config?.sandbox?.resources ? { defaultSize: config.sandbox.resources } : {}),
|
|
210
|
+
...(config?.priorityClasses?.sandbox ? { priorityClassName: config.priorityClasses.sandbox } : {}),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
// A held run's provision names the current Assembly when its image is not in the map.
|
|
214
|
+
currentAssembly: () => currentHash,
|
|
215
|
+
// Where a provision records the Repos it names, before the CR names them.
|
|
216
|
+
repos,
|
|
217
|
+
signingKey,
|
|
218
|
+
namespace,
|
|
219
|
+
client,
|
|
220
|
+
watch,
|
|
221
|
+
});
|
|
137
222
|
}
|
|
138
223
|
|
|
224
|
+
// The Repo passes run one after another on one chain, so the newest pass always finishes last:
|
|
225
|
+
// a slow boot pass can never reconcile after a publish has bound other Repos.
|
|
226
|
+
// A pass binds the Repos of every HELD Assembly's Machines, not the current one's alone
|
|
227
|
+
// (ADR-0051, ADR-0069): a run under a held Assembly mounts the Repos its own Machine binds, so
|
|
228
|
+
// they stay bound, and the boot restates their spec, while a live run holds that Assembly.
|
|
229
|
+
// Read when the pass runs, so it sees the holds of that moment.
|
|
230
|
+
let held: (() => Promise<AnyStateMachine[]>) | undefined;
|
|
231
|
+
let passes: Promise<void> = Promise.resolve();
|
|
232
|
+
const repoPass = (port: RepoResources): void => {
|
|
233
|
+
passes = passes.then(async () => {
|
|
234
|
+
let repos = carried.repos;
|
|
235
|
+
try {
|
|
236
|
+
if (held) repos = partsOf(await held()).repos;
|
|
237
|
+
} catch (err) {
|
|
238
|
+
opts.announce(JSON.stringify({ repos: [], error: (err as Error).message }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
await ensureBound(port, repos, opts.announce);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
139
245
|
const inst = await startInstance({
|
|
140
246
|
dir: opts.dir,
|
|
247
|
+
assembly: resolved,
|
|
141
248
|
port,
|
|
142
249
|
hostname: env.HOST ?? "0.0.0.0",
|
|
143
250
|
instanceToken,
|
|
251
|
+
publishToken,
|
|
144
252
|
signingKey,
|
|
145
253
|
sandbox,
|
|
146
254
|
dataPlane,
|
|
255
|
+
// The door reads the same image map a provision reads (ADR-0069): a `file:` context whose
|
|
256
|
+
// digest it lacks is refused at the publish rather than at the first provision.
|
|
257
|
+
...(dataPlane
|
|
258
|
+
? { imageMap: () => readImageRefs(join(IMAGES_MOUNT, IMAGES_KEY)), mounts: () => readMountDigests() }
|
|
259
|
+
: {}),
|
|
260
|
+
// A publish landed (ADR-0069): its config is the one that applies from now on, and its
|
|
261
|
+
// Machines may bind other Repos, so the Repo pass runs again. Synchronous: the walk is over
|
|
262
|
+
// the Machines the door already imported.
|
|
263
|
+
onPublish: ({ current: published, config: next, workflows }) => {
|
|
264
|
+
config = next;
|
|
265
|
+
bound = true;
|
|
266
|
+
currentHash = published.hash;
|
|
267
|
+
carried = partsOf(workflows.map((w) => w.machine));
|
|
268
|
+
if (carried.composesSandbox) watch?.start();
|
|
269
|
+
if (repos) repoPass(repos);
|
|
270
|
+
},
|
|
271
|
+
// The Assemblies the hourly sweep let go (ADR-0069): a run that finished released its hold.
|
|
272
|
+
onCollect: (collected) => opts.announce(JSON.stringify({ collected })),
|
|
273
|
+
// The restore a boot with no current Assembly deferred, run by the first publish that lands
|
|
274
|
+
// (ADR-0069): the runs it did not resume ride one line, as the boot's do (ADR-0030).
|
|
275
|
+
onRestore: ({ assembly, lost, drifted, failed }) => {
|
|
276
|
+
if (!lost.length && !drifted.length && !failed.length) return;
|
|
277
|
+
opts.announce(
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
assembly,
|
|
280
|
+
...(lost.length ? { lost } : {}),
|
|
281
|
+
...(drifted.length ? { drifted } : {}),
|
|
282
|
+
...(failed.length ? { failed } : {}),
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
},
|
|
147
286
|
// Read per request, never snapshotted: the Repos as the cluster reports them right now. An
|
|
148
287
|
// instance without a data plane reports none — `GET /repos` answers `{ dataPlane: false,
|
|
149
288
|
// repos: [] }` (http.ts), and what its dropped Machines left behind is `jr2 gc`'s to name.
|
|
@@ -151,37 +290,94 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
|
|
|
151
290
|
// The pod's route out (ADR-0053), same shape: read per request, off the port.
|
|
152
291
|
...(fetches ? { fetchRepo: (name: string, identity: string) => fetches.fetch(name, identity) } : {}),
|
|
153
292
|
// Where a Menu-only Turn runs (ADR-0031): the Instance Harness's deterministic Service DNS.
|
|
154
|
-
// `jr2 up` converges the
|
|
293
|
+
// `jr2 up` converges the StatefulSet behind it whenever any definition declares
|
|
155
294
|
// `workspace: "none"`, so deployed, the address exists exactly when it is needed.
|
|
156
295
|
instanceHarness: namespace
|
|
157
296
|
? `http://${INSTANCE_HARNESS_SERVICE}.${namespace}.svc:${INSTANCE_HARNESS_PORT}`
|
|
158
297
|
: undefined,
|
|
298
|
+
// Where an http Effect runs (ADR-0068): the Instance Effector's headless Service. `jr2 up`
|
|
299
|
+
// converges it whenever a registered Machine carries an http Effect.
|
|
300
|
+
...(namespace ? { instanceEffector: `${INSTANCE_EFFECTOR}.${namespace}.svc:${EFFECTOR_PORT}` } : {}),
|
|
301
|
+
// Where a push, a file-body http or a credentialed command runs (ADR-0068): a Sandbox's Effector, by
|
|
302
|
+
// the headless Service the provision writes beside it when `jr2 up` placed an Effect there.
|
|
303
|
+
...(namespace
|
|
304
|
+
? {
|
|
305
|
+
sandboxEffector: (sandbox: string) =>
|
|
306
|
+
`${sandboxEffectorService(sandbox)}.${namespace}.svc:${SANDBOX_EFFECTOR_PORT}`,
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
159
309
|
});
|
|
310
|
+
held = inst.heldMachines;
|
|
311
|
+
// The watch ends with the instance: its stream is the one thing here that never settles alone.
|
|
312
|
+
if (watch) {
|
|
313
|
+
const close = inst.close;
|
|
314
|
+
inst.close = async () => {
|
|
315
|
+
watch!.stop();
|
|
316
|
+
await close();
|
|
317
|
+
};
|
|
318
|
+
}
|
|
160
319
|
// Resumed runs are routine and stay quiet; runs this boot did NOT resume are not, so they ride
|
|
161
320
|
// the announce line (ADR-0030) — the one thing every boot prints, whatever is reading it. Without
|
|
162
321
|
// this, `drifted` is only reachable by asking after a run id nobody knows to ask about.
|
|
163
322
|
const { lost, drifted, failed } = inst.restored;
|
|
323
|
+
const chartCounts = inst.charts;
|
|
164
324
|
opts.announce(
|
|
165
325
|
JSON.stringify({
|
|
166
326
|
url: inst.url,
|
|
167
327
|
workflows: inst.workflows,
|
|
328
|
+
// Which Assembly this process serves (ADR-0069), and the Kit version it accepts at the door.
|
|
329
|
+
assembly: inst.assembly?.hash ?? null,
|
|
330
|
+
kitVersion: KIT_VERSION,
|
|
331
|
+
...(inst.assemblyError !== undefined ? { assemblyError: inst.assemblyError } : {}),
|
|
168
332
|
...(lost.length ? { lost } : {}),
|
|
169
333
|
...(drifted.length ? { drifted } : {}),
|
|
170
334
|
...(failed.length ? { failed } : {}),
|
|
335
|
+
// The live runs a boot with no current Assembly left untouched; the first publish restores them.
|
|
336
|
+
...(inst.waiting.length ? { waiting: inst.waiting } : {}),
|
|
337
|
+
...(inst.collected.length ? { collected: inst.collected } : {}),
|
|
338
|
+
// The named Charts (ADR-0070), when any exists: how many Chart files and runtime Charts, and
|
|
339
|
+
// the ones that cannot run now, with why.
|
|
340
|
+
...(chartCounts.files || chartCounts.runtime ? { charts: chartCounts } : {}),
|
|
171
341
|
}),
|
|
172
342
|
);
|
|
173
343
|
// After serving, never awaited: the bound Repos' resources (ADR-0051). Serving does not wait on
|
|
174
344
|
// the cluster — a run whose Repo the boot could not record still finds its provision ensuring
|
|
175
345
|
// it again — and a refusal is one announced line per Repo, in ADR-0048's shape. A deployed
|
|
176
346
|
// instance that binds nothing still runs this: the walk names no Repo, so the reconcile is the
|
|
177
|
-
// whole of it, and every resource an earlier deploy bound becomes evictable.
|
|
178
|
-
|
|
347
|
+
// whole of it, and every resource an earlier deploy bound becomes evictable. A boot with no
|
|
348
|
+
// current Assembly does not: an empty walk there means "unknown", not "binds nothing", and
|
|
349
|
+
// unlabeling every Repo would let `jr2 gc` evict them until the publish.
|
|
350
|
+
if (repos && inst.assembly) repoPass(repos);
|
|
179
351
|
return inst;
|
|
180
352
|
}
|
|
181
353
|
|
|
354
|
+
/** What a port that reads the config says while no current Assembly binds one (ADR-0069). */
|
|
355
|
+
export const NO_CONFIG =
|
|
356
|
+
"no current Assembly: no config applies, so this Orchestrator provisions nothing — `jr2 up` publishes one";
|
|
357
|
+
|
|
358
|
+
/** The one Sandbox watch, started on first use or by {@link LazyWatch.start}. */
|
|
359
|
+
type LazyWatch = SandboxWatch & { start(): void };
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A watch that starts on its first use (ADR-0063, ADR-0069). The data plane is wired on every
|
|
363
|
+
* deployed boot, but a cluster with no operator serves no Sandbox kind: a watch started there
|
|
364
|
+
* would only log its retries. Every method but `stop` starts it.
|
|
365
|
+
*/
|
|
366
|
+
function lazyWatch(begin: () => SandboxWatch): LazyWatch {
|
|
367
|
+
let inner: SandboxWatch | undefined;
|
|
368
|
+
const live = () => (inner ??= begin());
|
|
369
|
+
return {
|
|
370
|
+
start: () => void live(),
|
|
371
|
+
synced: () => live().synced(),
|
|
372
|
+
get: (name) => live().get(name),
|
|
373
|
+
subscribe: (name, listener) => live().subscribe(name, listener),
|
|
374
|
+
stop: () => inner?.stop(),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
182
378
|
/**
|
|
183
379
|
* The boot's half of ADR-0051's "the Orchestrator creates Repo resources": one per identity the
|
|
184
|
-
*
|
|
380
|
+
* Machines of the held Assemblies bind (ADR-0069), so statically known repositories are KNOWN before a run asks (the
|
|
185
381
|
* cache agent probes each; a node clones on first demand) — then
|
|
186
382
|
* the bound label reconciled, so a slot unbound since the last deploy is a Repo `jr2 gc` may
|
|
187
383
|
* evict. The boot is the ONE writer of a bound resource's spec: a redeploy that moved a url or a
|