nylorun 0.5.0-beta → 0.7.0-beta
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/CHANGELOG.md +27 -0
- package/README.md +72 -16
- package/dist/stack/commands.d.ts +4 -1
- package/dist/stack/commands.js +138 -34
- package/dist/stack/compose-file.d.ts +6 -3
- package/dist/stack/compose-file.js +37 -11
- package/dist/stack/docker.d.ts +10 -0
- package/dist/stack/docker.js +63 -0
- package/dist/stack/prepare.js +1 -1
- package/dist/stack/stacks.d.ts +8 -0
- package/dist/stack/stacks.js +19 -0
- package/dist/stack/studio-login.d.ts +4 -3
- package/dist/stack/studio-login.js +5 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# nylorun
|
|
2
2
|
|
|
3
|
+
## 0.7.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9af30d8: **The Studio proxy is removed, and Restate runs with its own memory defaults.** Each Tenant's Studio is at `http://localhost:<port>` again (`nylorun studio --tenant <name>` opens it signed in): its own session cookie already keeps it apart from other Studios, so the proxy only added a container and a second address. The 256 MiB RocksDB cap did not lower Restate's memory, so it is gone. The first command of this release removes the proxy that 0.6 started (`nylorun-proxy` and `~/.nylorun/proxy/`); `NYLORUN_PROXY_PORT`, `NYLORUN_PROXY_DISABLED`, `studio.proxyUrl` and Studio's `NYLORUN_STUDIO_PUBLIC_ORIGINS` are gone.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Pin studio to the tested release.
|
|
12
|
+
|
|
13
|
+
## 0.6.0-beta
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0cbd5c9: **Breaking (`nylorun`): readable Docker names, several Tenants on one machine, and Studio at `http://<name>.localhost:4160`.** See MIGRATION.md.
|
|
18
|
+
|
|
19
|
+
- **Breaking (`nylorun`): names.** Every container, the network and every volume carries the Tenant's Compose project: containers `nylorun-<name>-postgres`, `-restate`, `-s2-lite`, `-gateway`, `-runtime`, `-studio` (were `…-1`), network `nylorun-<name>` (was `nylorun-<name>_default`), volumes `nylorun-<name>-postgres`, `-restate`, `-s2-lite`, `-workspaces` (were `nylorun-<name>_postgres`, …), each labelled `dev.nylorun.tenant: <name>`. The Compose service `s2` is now `s2-lite` (`nylorun logs s2-lite`).
|
|
20
|
+
- **Breaking (`nylorun`): Tenants created by 0.5 start fresh.** `start` on a Tenant whose data is in the old volumes exits 3 without starting it and names them; `nylorun reset --tenant <name>` starts it anew. `reset` and `delete` also remove the old volumes and the old network.
|
|
21
|
+
- **Studio proxy.** One Caddy container per machine (`nylorun-proxy`, files in `~/.nylorun/proxy/`) gives each Tenant's Studio the address `http://<name>.localhost:<port>` (`NYLORUN_PROXY_PORT`, 4160 or a free port chosen once), on `127.0.0.1` and `[::1]` (IPv4 only, saying so, when Docker refuses `::1`). It holds no Tenant data and routes browsers only. `start` brings it up when Studio starts, prints Studio's proxy URL and signs in there (on Studio's own port when the proxy does not answer); a proxy failure never fails `start`. `status` shows both URLs, `ls` and `nylorun studio` the proxy's. `NYLORUN_PROXY_DISABLED=1` turns it off; a Tenant under `NYLORUN_HOME` or `NYLORUN_COMPOSE_PROJECT` does not use it. `doctor` has a `proxy` row.
|
|
22
|
+
- **Several Tenants.** Restate's RocksDB memory is capped at 256 MiB, so a Tenant uses about 600–700 MB (was about 1.3 GB). `nylorun ls` has a `MEMORY` column (`memoryBytes` in `--json`). `start` names the other running Tenants and their memory. `nylorun stop --all` stops every running Tenant and the proxy, keeping their volumes.
|
|
23
|
+
- **`@nylorun/studio`.** `NYLORUN_STUDIO_PUBLIC_ORIGINS` lists exact `http:` origins Studio also serves (Host and `Origin` checks); a login token's URL is on the origin it was minted on; `NYLORUN_STUDIO_SESSION_COOKIE` names the session cookie (default `nylorun_studio_session`; `nylorun` sets `nylorun_studio_<name>`, so Studios on one host keep their own sessions); the `421` answer lists the served origins.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Pin runtime to the tested release.
|
|
28
|
+
- Pin studio to the tested release.
|
|
29
|
+
|
|
3
30
|
## 0.5.0-beta
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ npx nylorun doctor # Node 24+, Docker, Compose v2, and the Tenan
|
|
|
36
36
|
```sh
|
|
37
37
|
nylorun up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
|
|
38
38
|
# create (first run) and start the Tenant; link the project; print the URLs; open Studio signed in
|
|
39
|
-
nylorun down|stop [--tenant <name>] # stop the containers; keep volumes
|
|
39
|
+
nylorun down|stop [--tenant <name> | --all] # stop the containers (--all: every Tenant's); keep volumes
|
|
40
40
|
nylorun status [--tenant <name>] [--json] # the Tenant, its Host root and id, services, endpoints, Runtime health
|
|
41
|
-
nylorun logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2, gateway, runtime, studio
|
|
41
|
+
nylorun logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2-lite, gateway, runtime, studio
|
|
42
42
|
nylorun studio [--tenant <name>] [--no-open] # sign a browser in to Studio on the Tenant; starts it if needed
|
|
43
43
|
nylorun reset [--tenant <name>] [--yes] # delete the Tenant's volumes, Tenant directory and vault key; the next start creates it anew
|
|
44
|
-
nylorun ls [--json] # the Tenants on this machine
|
|
44
|
+
nylorun ls [--json] # the Tenants on this machine, with their state, memory and URLs
|
|
45
45
|
nylorun delete <tenant> --yes # remove a Tenant: containers, volumes, Host root and vault key
|
|
46
46
|
nylorun doctor [--json] # prerequisites and the Tenant's health
|
|
47
47
|
nylorun telemetry [status|enable|disable] # Studio's anonymous usage analytics
|
|
@@ -78,6 +78,42 @@ name). nylorun 0.4 kept Host roots in `~/.nylorun/stacks/`: the first command
|
|
|
78
78
|
of this release moves them to `~/.nylorun/tenants/`, keeping their Compose
|
|
79
79
|
projects and volumes.
|
|
80
80
|
|
|
81
|
+
### Several Tenants
|
|
82
|
+
|
|
83
|
+
Tenants run side by side, each on its own ports, containers, network and
|
|
84
|
+
volumes ([The containers](#the-containers)); a running Tenant uses about
|
|
85
|
+
1.2 GB. Each Studio is on its own port, `http://localhost:<port>`, with its own
|
|
86
|
+
session cookie, so two Studios in one browser stay signed in;
|
|
87
|
+
`nylorun studio --tenant <name>` opens any Tenant's Studio signed in. `nylorun ls`
|
|
88
|
+
lists them with their state, the memory their containers use (`MEMORY`: the sum
|
|
89
|
+
of `docker stats` over the containers labelled `dev.nylorun.tenant=<name>`, `-`
|
|
90
|
+
when stopped; `memoryBytes` in `--json`, `null` when stopped or unknown), the
|
|
91
|
+
Runtime's URL and Studio's:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
TENANT STATE MEMORY RUNTIME STUDIO PROJECT
|
|
95
|
+
api running 1.2 GB http://localhost:8790 http://localhost:4162 /Users/me/api
|
|
96
|
+
default stopped - http://localhost:8787 http://localhost:4161 -
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When other Tenants are running, `start` says so after its summary, on stderr
|
|
100
|
+
(without the size when Docker does not report it):
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Also running: default, api (about 2.4 GB). "nylorun stop --all" stops them all.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`nylorun stop --all` stops every running Tenant on this machine, keeping their
|
|
107
|
+
volumes; it cannot be combined with `--tenant` (exit 2).
|
|
108
|
+
|
|
109
|
+
A Tenant created by nylorun 0.5 keeps its data in volumes Compose named
|
|
110
|
+
`<project>_postgres`, `<project>_restate`, `<project>_s2` and
|
|
111
|
+
`<project>_workspaces`; this release names them `<project>-postgres`, … (see
|
|
112
|
+
[The containers](#the-containers)). `start` on such a Tenant exits 3 without
|
|
113
|
+
starting it, naming the old volumes: run `nylorun reset --tenant <name>` to
|
|
114
|
+
start it fresh. `reset` and `delete` also remove the old volumes (and the old
|
|
115
|
+
network `<project>_default`).
|
|
116
|
+
|
|
81
117
|
A Tenant refuses only a Runtime older than its own database: `start` records
|
|
82
118
|
the pinned Runtime version in `host.json` (`runtimeVersion`) and exits 5 when
|
|
83
119
|
this nylorun pins an older one. Update nylorun, or pass `--allow-downgrade`
|
|
@@ -119,9 +155,21 @@ The Runtime registers the derived principals of `NYLORUN_DERIVED_PRINCIPALS`
|
|
|
119
155
|
## The containers
|
|
120
156
|
|
|
121
157
|
`nylorun start` writes `compose.yaml` and `.env` (mode 0600) under
|
|
122
|
-
`~/.nylorun/tenants/<name>/docker/` on
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
`~/.nylorun/tenants/<name>/docker/` on every start; `.env` keeps the ports and
|
|
159
|
+
secrets chosen on the first run. The services are `postgres`, `restate`,
|
|
160
|
+
`s2-lite`, `gateway`, `runtime` and `studio`. Container, network and volume
|
|
161
|
+
names are global on the Docker engine, so each carries the Tenant's Compose
|
|
162
|
+
project, and each has the label `dev.nylorun.tenant: <name>`. For Tenant `shop`:
|
|
163
|
+
|
|
164
|
+
| Thing | Name |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| Containers | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-gateway`, `nylorun-shop-runtime`, `nylorun-shop-studio` |
|
|
167
|
+
| Network | `nylorun-shop` |
|
|
168
|
+
| Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-workspaces` |
|
|
169
|
+
|
|
170
|
+
Postgres initialises the Tenant's database with C collation. Restate runs with
|
|
171
|
+
its own defaults. A running Tenant uses about 1.2 GB, most of it Restate; stop
|
|
172
|
+
the Tenants you are not using (`nylorun stop`, or `nylorun stop --all`). The Runtime and Studio images are pinned by
|
|
125
173
|
this release (`package.json` `nylorun.runtime` and `nylorun.studio`);
|
|
126
174
|
`NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them (local builds,
|
|
127
175
|
CI). Ports publish on loopback only: the Runtime on `8787`, its operator
|
|
@@ -129,15 +177,21 @@ listener (Admin API) on `8788`, Studio on `4161` and the Restate UI on `9070`,
|
|
|
129
177
|
or free ports chosen on the Tenant's first start, avoiding the ports other
|
|
130
178
|
Tenants keep, and kept in its `.env`.
|
|
131
179
|
|
|
132
|
-
`start`
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
180
|
+
`start`, `status`, `ls` and `nylorun studio` give Studio as
|
|
181
|
+
`http://localhost:<port>` (`studio.url` in `status --json`). Studio has no
|
|
182
|
+
password: in a terminal (not in CI, and not with `--no-open`), `start` asks the
|
|
183
|
+
Studio container for a single-use login token with the admin key (`POST
|
|
184
|
+
/_studio/login-tokens`) and opens `http://localhost:<port>/login?token=…` on the
|
|
185
|
+
Tenant's page in the browser. That sets a session cookie for 30 days, which
|
|
186
|
+
survives Studio restarts, so the printed URL keeps working in that browser. The
|
|
187
|
+
token itself is never printed unless no browser starts. Otherwise `start` says
|
|
188
|
+
to run `nylorun studio`, which signs a browser in the same way; `nylorun studio
|
|
189
|
+
--no-open` prints the login URL (it works once, for two minutes) instead.
|
|
190
|
+
|
|
191
|
+
Browsers share cookies across the ports of one host, so each Tenant's Studio
|
|
192
|
+
sets its own session cookie, `nylorun_studio_<name>`
|
|
193
|
+
(`NYLORUN_STUDIO_SESSION_COOKIE`): signing in to one Studio does not sign you
|
|
194
|
+
out of another.
|
|
141
195
|
|
|
142
196
|
### Embedding Studio in a desktop app
|
|
143
197
|
|
|
@@ -201,7 +255,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
|
|
|
201
255
|
| 0 | Success |
|
|
202
256
|
| 1 | Generic failure, including a missing Docker or Compose v2 |
|
|
203
257
|
| 2 | Usage error (including no Tenant selected, or `delete` without `--yes`), or a removed or moved command |
|
|
204
|
-
| 3 | `status`: the Runtime is not answering; `stop`/`logs`: no Tenant yet; `delete`: no such Tenant |
|
|
258
|
+
| 3 | `status`: the Runtime is not answering; `stop`/`logs`: no Tenant yet; `delete`: no such Tenant; `start`: a Tenant created by nylorun 0.5 (reset it) |
|
|
205
259
|
| 4 | The Runtime port is held by another Host |
|
|
206
260
|
| 5 | `up`/`start`/`studio`: this nylorun pins a Runtime older than the one that last ran the Tenant; update nylorun, or `start --allow-downgrade` |
|
|
207
261
|
| 7 | The containers, the Tenant or Studio did not become ready (an unavailable Tenant's cause is printed) |
|
|
@@ -218,5 +272,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
|
|
|
218
272
|
| Port conflict | Change `NYLORUN_PORT` / `NYLORUN_STUDIO_PORT` in `~/.nylorun/tenants/<name>/docker/.env` |
|
|
219
273
|
| Logs | `nylorun logs runtime -f` |
|
|
220
274
|
| Studio login expired | `nylorun studio` |
|
|
275
|
+
| Which Studio is which | `nylorun ls` lists each Tenant's Studio URL; `nylorun studio --tenant <name>` opens one signed in |
|
|
276
|
+
| `Tenant … was created by nylorun 0.5` (exit 3) | `nylorun reset --tenant <name>` (its data starts fresh) |
|
|
221
277
|
|
|
222
278
|
See [MIGRATION.md](../MIGRATION.md).
|
package/dist/stack/commands.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { HostTenant } from "@nylorun/core/contracts";
|
|
|
2
2
|
import { type ComposeService, type DockerRunner } from "./docker.js";
|
|
3
3
|
import type { PortProbe } from "./ports.js";
|
|
4
4
|
import { type FetchLike } from "./studio-login.js";
|
|
5
|
-
export declare const STACK_SERVICES: readonly ["postgres", "restate", "s2", "gateway", "runtime", "studio"];
|
|
5
|
+
export declare const STACK_SERVICES: readonly ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
|
|
6
6
|
export declare const stackUsage: string;
|
|
7
7
|
export interface StackDeps {
|
|
8
8
|
/** Environment snapshot (NYLORUN_HOME, NYLORUN_TENANT, image overrides, NYLORUN_COMPOSE_PROJECT). */
|
|
@@ -73,6 +73,7 @@ export interface StackStatus {
|
|
|
73
73
|
/** The Tenant as the Runtime reports it, while it answers. */
|
|
74
74
|
tenant?: StackTenant;
|
|
75
75
|
studio: {
|
|
76
|
+
/** `http://localhost:<port>`: also what an embedding app frames (same site as its own localhost). */
|
|
76
77
|
url?: string;
|
|
77
78
|
state: string;
|
|
78
79
|
/** Exact origins that may show Studio in a frame (Studio §8.9). */
|
|
@@ -105,6 +106,8 @@ export interface StackEndpoints {
|
|
|
105
106
|
hostId: string;
|
|
106
107
|
adminKey: string;
|
|
107
108
|
studioPort: number;
|
|
109
|
+
/** `http://localhost:<studio port>` */
|
|
110
|
+
studioUrl: string;
|
|
108
111
|
/** Studio is running and healthy. */
|
|
109
112
|
studioUp: boolean;
|
|
110
113
|
/** This call started the Runtime (it was not running before). */
|
package/dist/stack/commands.js
CHANGED
|
@@ -8,28 +8,28 @@ import { readProjectCredentials, readProjectLink, writeProjectCredentials, write
|
|
|
8
8
|
import { findProjectRoot } from "../project/root.js";
|
|
9
9
|
import { seedTenant } from "../project/seed.js";
|
|
10
10
|
import { readTelemetry, STUDIO_ANALYTICS_ID, TELEMETRY_NOTICE, telemetryDecision, writeTelemetry, } from "../telemetry.js";
|
|
11
|
-
import { dockerPreflight, parseComposePs, } from "./docker.js";
|
|
11
|
+
import { dockerPreflight, formatBytes, parseComposePs, tenantMemory, } from "./docker.js";
|
|
12
12
|
import { readAdminKey, readHostConfig, STACK_CLIENT_HOST } from "./host-files.js";
|
|
13
13
|
import { runtimeImageOverridden, stackImages } from "./images.js";
|
|
14
14
|
import { stackPaths } from "./paths.js";
|
|
15
15
|
import { prepareStack, readStackEnv } from "./prepare.js";
|
|
16
|
-
import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
|
|
16
|
+
import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, removeOldProxy, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
|
|
17
17
|
import { mintStudioLogin, studioOrigin } from "./studio-login.js";
|
|
18
|
-
export const STACK_SERVICES = ["postgres", "restate", "s2", "gateway", "runtime", "studio"];
|
|
19
|
-
const CORE_SERVICES = ["postgres", "restate", "s2", "gateway", "runtime"];
|
|
18
|
+
export const STACK_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
|
|
19
|
+
const CORE_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime"];
|
|
20
20
|
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]
|
|
21
21
|
start the project's Tenant, creating it and the Project link (.nylorun/link.json,
|
|
22
22
|
credentials.json) on the first run; print the Runtime and Studio URLs and open Studio
|
|
23
23
|
signed in (in a terminal). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
24
24
|
starts the default Tenant (or --tenant's) without linking the current directory
|
|
25
|
-
down|stop [--tenant <name>]
|
|
25
|
+
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
|
|
26
26
|
status [--tenant <name>] [--json] the Tenant, its id, services, endpoints and Runtime health
|
|
27
27
|
logs [service] [--tenant <name>] [-f] [--tail <n>]
|
|
28
28
|
container logs (${STACK_SERVICES.join(", ")})
|
|
29
29
|
studio [--tenant <name>] [--no-open]
|
|
30
30
|
sign a browser in to Studio on the Tenant; --no-open prints the login URL; start the Tenant if it is stopped
|
|
31
31
|
reset [--tenant <name>] [--yes] delete the Tenant's data (volumes, tenant/ and vault key); the next start creates it anew
|
|
32
|
-
ls [--json] the Tenants on this machine
|
|
32
|
+
ls [--json] the Tenants on this machine, with their memory in use
|
|
33
33
|
delete <tenant> --yes remove a Tenant: containers, volumes and its Host root, with the vault key (KEK)`;
|
|
34
34
|
const usageError = (message) => new CliError(message, 2);
|
|
35
35
|
/** The Compose project of Tenant `name`: `nylorun-<name>`, or `NYLORUN_COMPOSE_PROJECT`. */
|
|
@@ -42,6 +42,38 @@ export function stackProject(env, name) {
|
|
|
42
42
|
function nylorunRoot(deps) {
|
|
43
43
|
return deps.nylorunRoot ?? defaultNylorunRoot();
|
|
44
44
|
}
|
|
45
|
+
/** Before every Tenant command: move 0.4's Host roots, remove 0.6's Studio proxy. */
|
|
46
|
+
async function tidyMachine(deps) {
|
|
47
|
+
const base = nylorunRoot(deps);
|
|
48
|
+
await moveStackRoots(base, deps.err);
|
|
49
|
+
await removeOldProxy(deps.docker, base, deps.err);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compose's names for a 0.5 Tenant's volumes (`<project>_postgres`, …) that exist on this
|
|
53
|
+
* engine, and whether this release's `<project>-postgres` does.
|
|
54
|
+
*/
|
|
55
|
+
async function oldVolumes(docker, project) {
|
|
56
|
+
const result = await docker.run(["volume", "ls", "--quiet", "--filter", `name=${project}`]);
|
|
57
|
+
const names = new Set(result.code === 0 ? result.stdout.split(/\s+/) : []);
|
|
58
|
+
return {
|
|
59
|
+
old: ["postgres", "restate", "s2", "workspaces"]
|
|
60
|
+
.map((volume) => `${project}_${volume}`)
|
|
61
|
+
.filter((name) => names.has(name)),
|
|
62
|
+
current: names.has(`${project}-postgres`),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** `docker compose down` (`reset`, `delete`), then drop a 0.5 Tenant's volumes and network. */
|
|
66
|
+
async function composeDown(ctx, args) {
|
|
67
|
+
const { docker } = ctx.deps;
|
|
68
|
+
const code = await docker.stream(args);
|
|
69
|
+
if (code !== 0)
|
|
70
|
+
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
71
|
+
const { old } = await oldVolumes(docker, ctx.project);
|
|
72
|
+
if (old.length) {
|
|
73
|
+
await docker.run(["volume", "rm", ...old]);
|
|
74
|
+
await docker.run(["network", "rm", `${ctx.project}_default`]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
45
77
|
async function tenantNames(deps) {
|
|
46
78
|
const names = (await listTenants(nylorunRoot(deps))).map((tenant) => tenant.name);
|
|
47
79
|
return names.length ? ` Tenants on this machine: ${names.join(", ")}.` : "";
|
|
@@ -285,10 +317,10 @@ async function refuseDowngrade(ctx, allowDowngrade) {
|
|
|
285
317
|
}
|
|
286
318
|
throw new CliError(`Refusing to start Runtime ${pinned} on Tenant ${ctx.name}: Runtime ${host.runtimeVersion} last ran it (${ctx.paths.config}), and a Runtime older than the Tenant's database cannot open its Tenant. Update nylorun (npx nylorun@latest start), or run "nylorun start --allow-downgrade" when both Runtimes use the same database schema.`, 5);
|
|
287
319
|
}
|
|
288
|
-
/** Ports
|
|
289
|
-
async function
|
|
290
|
-
const others = (await listTenants(
|
|
291
|
-
.filter((tenant) => resolve(tenant.root) !==
|
|
320
|
+
/** Ports the Tenants on this machine keep in their `.env` (but the one under `except`). */
|
|
321
|
+
async function tenantPorts(base, except) {
|
|
322
|
+
const others = (await listTenants(base))
|
|
323
|
+
.filter((tenant) => resolve(tenant.root) !== except)
|
|
292
324
|
.map((tenant) => stackPaths(tenant.root));
|
|
293
325
|
const reserved = new Set();
|
|
294
326
|
for (const paths of others) {
|
|
@@ -304,13 +336,21 @@ async function reservedPorts(ctx) {
|
|
|
304
336
|
}
|
|
305
337
|
return reserved;
|
|
306
338
|
}
|
|
339
|
+
/** Ports other Tenants keep, so a new Tenant avoids them. */
|
|
340
|
+
async function reservedPorts(ctx) {
|
|
341
|
+
return await tenantPorts(nylorunRoot(ctx.deps), ctx.paths.root);
|
|
342
|
+
}
|
|
307
343
|
async function bringUp(ctx, options) {
|
|
308
344
|
const { deps } = ctx;
|
|
309
345
|
await refuseLauncherRuntime(ctx);
|
|
310
346
|
const overridden = runtimeImageOverridden(deps.env);
|
|
311
347
|
if (!overridden)
|
|
312
348
|
await refuseDowngrade(ctx, options.allowDowngrade ?? false);
|
|
313
|
-
const
|
|
349
|
+
const legacy = await oldVolumes(deps.docker, ctx.project);
|
|
350
|
+
if (legacy.old.length && !legacy.current)
|
|
351
|
+
throw new CliError(`Tenant ${ctx.name} was created by nylorun 0.5; its data is in the old volumes ${legacy.old.join(", ")}. Run "nylorun reset --tenant ${ctx.name}" to start it fresh; that also removes the old volumes (or drop them yourself with "docker volume rm ${legacy.old.join(" ")}").`, 3);
|
|
352
|
+
const base = nylorunRoot(deps);
|
|
353
|
+
const telemetry = await readTelemetry(base);
|
|
314
354
|
const analytics = telemetryDecision(deps.env, telemetry).enabled;
|
|
315
355
|
const prepared = await prepareStack({
|
|
316
356
|
paths: ctx.paths,
|
|
@@ -365,25 +405,30 @@ async function bringUp(ctx, options) {
|
|
|
365
405
|
adminUrl,
|
|
366
406
|
hostId: prepared.host.hostId,
|
|
367
407
|
studioPort: prepared.env.studioPort,
|
|
408
|
+
studioUrl: studioOrigin(prepared.env.studioPort),
|
|
368
409
|
studioStarted,
|
|
369
410
|
adminKey: prepared.adminKey,
|
|
370
411
|
};
|
|
371
412
|
}
|
|
413
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
414
|
+
/** A login URL on Studio's own port, retried while it starts. */
|
|
372
415
|
async function tryStudioLogin(ctx, studioPort, adminKey) {
|
|
373
|
-
const
|
|
416
|
+
const { deps } = ctx;
|
|
417
|
+
const origin = studioOrigin(studioPort);
|
|
418
|
+
const deadline = Date.now() + (deps.loginTimeoutMs ?? 15_000);
|
|
374
419
|
let lastError = "";
|
|
375
420
|
for (;;) {
|
|
376
421
|
try {
|
|
377
|
-
return await mintStudioLogin({ fetch:
|
|
422
|
+
return await mintStudioLogin({ fetch: deps.fetch, origin, adminKey });
|
|
378
423
|
}
|
|
379
424
|
catch (error) {
|
|
380
|
-
lastError =
|
|
425
|
+
lastError = errorText(error);
|
|
381
426
|
}
|
|
382
427
|
if (Date.now() > deadline)
|
|
383
428
|
break;
|
|
384
|
-
await sleep(
|
|
429
|
+
await sleep(deps.pollMs ?? 500);
|
|
385
430
|
}
|
|
386
|
-
|
|
431
|
+
deps.err(`Warning: Studio at ${origin} is not reachable for a login (${lastError}).`);
|
|
387
432
|
return undefined;
|
|
388
433
|
}
|
|
389
434
|
/** Printed by `start` when it does not open Studio itself. */
|
|
@@ -483,7 +528,7 @@ async function start(deps, args) {
|
|
|
483
528
|
deps.out(`Tenant ${ctx.name} (${tenant.id})`);
|
|
484
529
|
deps.out(`Runtime ${started.runtimeUrl}`);
|
|
485
530
|
if (started.studioStarted) {
|
|
486
|
-
deps.out(`Studio ${
|
|
531
|
+
deps.out(`Studio ${started.studioUrl}`);
|
|
487
532
|
if (opensBrowser(deps, flags)) {
|
|
488
533
|
const login = await tryStudioLogin(ctx, started.studioPort, started.adminKey);
|
|
489
534
|
if (login)
|
|
@@ -492,13 +537,37 @@ async function start(deps, args) {
|
|
|
492
537
|
else
|
|
493
538
|
deps.err(STUDIO_SIGN_IN_HINT);
|
|
494
539
|
}
|
|
540
|
+
await alsoRunning(ctx);
|
|
495
541
|
return 0;
|
|
496
542
|
}
|
|
543
|
+
/** After `start`: the machine's other running Tenants and their memory, on stderr. */
|
|
544
|
+
async function alsoRunning(ctx) {
|
|
545
|
+
const { deps } = ctx;
|
|
546
|
+
const projects = await composeProjects(deps.docker);
|
|
547
|
+
if (!projects)
|
|
548
|
+
return;
|
|
549
|
+
const others = (await listTenants(nylorunRoot(deps)))
|
|
550
|
+
.map((tenant) => tenant.name)
|
|
551
|
+
.filter((name) => name !== ctx.name && projects.get(`nylorun-${name}`) === "running");
|
|
552
|
+
if (others.length === 0)
|
|
553
|
+
return;
|
|
554
|
+
const memory = await tenantMemory(deps.docker);
|
|
555
|
+
const sizes = others.map((name) => memory?.get(name));
|
|
556
|
+
const total = sizes.every((size) => size !== undefined)
|
|
557
|
+
? sizes.reduce((sum, size) => sum + size, 0)
|
|
558
|
+
: undefined;
|
|
559
|
+
deps.err(`Also running: ${others.join(", ")}${total === undefined ? "" : ` (about ${formatBytes(total)})`}. "nylorun stop --all" stops them all.`);
|
|
560
|
+
}
|
|
497
561
|
async function stop(deps, args) {
|
|
498
|
-
const usage = "nylorun stop [--tenant <name>]";
|
|
499
|
-
const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
|
|
562
|
+
const usage = "nylorun stop [--tenant <name> | --all]";
|
|
563
|
+
const flags = parseStackFlags(args, { booleans: ["--all"], values: ["--tenant"] }, usage);
|
|
500
564
|
if (flags.rest.length)
|
|
501
565
|
throw usageError(`Usage: ${usage}`);
|
|
566
|
+
if (flags.booleans.has("--all")) {
|
|
567
|
+
if (flags.values.has("--tenant"))
|
|
568
|
+
throw usageError(`--all and --tenant are exclusive. Usage: ${usage}`);
|
|
569
|
+
return await stopAll(deps);
|
|
570
|
+
}
|
|
502
571
|
const ctx = await selectStack(deps, nameOption(flags));
|
|
503
572
|
requireStackFiles(ctx);
|
|
504
573
|
await dockerPreflight(deps.docker);
|
|
@@ -508,6 +577,28 @@ async function stop(deps, args) {
|
|
|
508
577
|
deps.out(`Stopped Tenant ${ctx.name}; its data is kept.`);
|
|
509
578
|
return 0;
|
|
510
579
|
}
|
|
580
|
+
/** `stop --all`: every running Tenant on this machine; volumes are kept. */
|
|
581
|
+
async function stopAll(deps) {
|
|
582
|
+
await dockerPreflight(deps.docker);
|
|
583
|
+
const projects = (await composeProjects(deps.docker)) ?? new Map();
|
|
584
|
+
const stopped = [];
|
|
585
|
+
for (const tenant of await listTenants(nylorunRoot(deps))) {
|
|
586
|
+
const project = `nylorun-${tenant.name}`;
|
|
587
|
+
if (projects.get(project) !== "running")
|
|
588
|
+
continue;
|
|
589
|
+
const paths = stackPaths(tenant.root);
|
|
590
|
+
const code = await deps.docker.stream(existsSync(paths.compose) && existsSync(paths.env)
|
|
591
|
+
? composeArgs({ project, paths }, "stop")
|
|
592
|
+
: ["compose", "--project-name", project, "stop"]);
|
|
593
|
+
if (code !== 0)
|
|
594
|
+
throw new CliError(`docker compose stop failed for Tenant ${tenant.name} (exit ${code}).`, 1);
|
|
595
|
+
stopped.push(tenant.name);
|
|
596
|
+
}
|
|
597
|
+
deps.out(stopped.length
|
|
598
|
+
? `Stopped Tenants ${stopped.join(", ")}; their data is kept.`
|
|
599
|
+
: "No Tenant was running.");
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
511
602
|
async function stackStatus(ctx) {
|
|
512
603
|
const base = {
|
|
513
604
|
name: ctx.name,
|
|
@@ -570,7 +661,7 @@ async function stackStatus(ctx) {
|
|
|
570
661
|
}
|
|
571
662
|
/** What `nylorun status` reports, as data (`nylorun doctor`). */
|
|
572
663
|
export async function readStackStatus(deps, options = {}) {
|
|
573
|
-
await
|
|
664
|
+
await tidyMachine(deps);
|
|
574
665
|
return await stackStatus(await selectStack(deps, options));
|
|
575
666
|
}
|
|
576
667
|
function describeTenant(tenant) {
|
|
@@ -655,9 +746,7 @@ async function reset(deps, args) {
|
|
|
655
746
|
}
|
|
656
747
|
if (existsSync(paths.compose) && existsSync(paths.env)) {
|
|
657
748
|
await dockerPreflight(deps.docker);
|
|
658
|
-
|
|
659
|
-
if (code !== 0)
|
|
660
|
-
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
749
|
+
await composeDown(ctx, composeArgs(ctx, "down", "--volumes", "--remove-orphans"));
|
|
661
750
|
}
|
|
662
751
|
await rm(paths.tenant, { recursive: true, force: true });
|
|
663
752
|
await mkdir(paths.tenant, { recursive: true, mode: 0o700 });
|
|
@@ -691,14 +780,19 @@ async function ls(deps, args) {
|
|
|
691
780
|
const base = nylorunRoot(deps);
|
|
692
781
|
const projects = await composeProjects(deps.docker);
|
|
693
782
|
const state = (project) => (projects ? (projects.get(project) ?? "stopped") : "unknown");
|
|
783
|
+
const memory = [...(projects?.values() ?? [])].includes("running")
|
|
784
|
+
? await tenantMemory(deps.docker)
|
|
785
|
+
: undefined;
|
|
694
786
|
const tenants = [];
|
|
695
787
|
for (const entry of await listTenants(base)) {
|
|
696
788
|
const persisted = (await readStackEnv(stackPaths(entry.root))) ?? {};
|
|
789
|
+
const tenantState = state(`nylorun-${entry.name}`);
|
|
697
790
|
tenants.push({
|
|
698
791
|
name: entry.name,
|
|
699
792
|
root: entry.root,
|
|
700
793
|
...(entry.record.project ? { project: entry.record.project } : {}),
|
|
701
|
-
state:
|
|
794
|
+
state: tenantState,
|
|
795
|
+
memoryBytes: (tenantState === "running" && memory?.get(entry.name)) || null,
|
|
702
796
|
...(persisted.runtimePort
|
|
703
797
|
? { runtimeUrl: `http://${STACK_CLIENT_HOST}:${persisted.runtimePort}` }
|
|
704
798
|
: {}),
|
|
@@ -719,10 +813,20 @@ async function ls(deps, args) {
|
|
|
719
813
|
deps.out('No Tenants on this machine. Run "npx nylorun start" in a project (or anywhere, for the default Tenant).');
|
|
720
814
|
return 0;
|
|
721
815
|
}
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
816
|
+
const rows = [
|
|
817
|
+
["TENANT", "STATE", "MEMORY", "RUNTIME", "STUDIO", "PROJECT"],
|
|
818
|
+
...tenants.map((tenant) => [
|
|
819
|
+
tenant.name,
|
|
820
|
+
tenant.state,
|
|
821
|
+
tenant.memoryBytes === null ? "-" : formatBytes(tenant.memoryBytes),
|
|
822
|
+
tenant.runtimeUrl ?? "-",
|
|
823
|
+
tenant.studioUrl ?? "-",
|
|
824
|
+
tenant.project ?? "-",
|
|
825
|
+
]),
|
|
826
|
+
];
|
|
827
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)) + 2);
|
|
828
|
+
for (const row of rows)
|
|
829
|
+
deps.out(row.map((cell, column) => (column < row.length - 1 ? cell.padEnd(widths[column]) : cell)).join(""));
|
|
726
830
|
return 0;
|
|
727
831
|
}
|
|
728
832
|
async function deleteStack(deps, args) {
|
|
@@ -740,11 +844,9 @@ async function deleteStack(deps, args) {
|
|
|
740
844
|
throw usageError(`nylorun delete removes Tenant ${name}: its containers, volumes and Host root ${ctx.paths.root}, with its vault key (KEK) and all its data. This cannot be undone; pass --yes to confirm.`);
|
|
741
845
|
await dockerPreflight(deps.docker);
|
|
742
846
|
const files = existsSync(ctx.paths.compose) && existsSync(ctx.paths.env);
|
|
743
|
-
|
|
847
|
+
await composeDown(ctx, files
|
|
744
848
|
? composeArgs(ctx, "down", "--volumes", "--remove-orphans")
|
|
745
849
|
: ["compose", "--project-name", ctx.project, "down", "--volumes", "--remove-orphans"]);
|
|
746
|
-
if (code !== 0)
|
|
747
|
-
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
748
850
|
await rm(ctx.paths.root, { recursive: true, force: true });
|
|
749
851
|
deps.out(`Deleted Tenant ${name}: its containers, volumes and Host root (vault key included).`);
|
|
750
852
|
return 0;
|
|
@@ -780,6 +882,7 @@ async function runningStack(ctx, options) {
|
|
|
780
882
|
hostId: host.hostId,
|
|
781
883
|
adminKey,
|
|
782
884
|
studioPort: persisted.studioPort,
|
|
885
|
+
studioUrl: studioOrigin(persisted.studioPort),
|
|
783
886
|
studioUp,
|
|
784
887
|
started: false,
|
|
785
888
|
};
|
|
@@ -799,6 +902,7 @@ async function ensureSelected(ctx, options) {
|
|
|
799
902
|
hostId: started.hostId,
|
|
800
903
|
adminKey: started.adminKey,
|
|
801
904
|
studioPort: started.studioPort,
|
|
905
|
+
studioUrl: started.studioUrl,
|
|
802
906
|
studioUp: started.studioStarted,
|
|
803
907
|
started: !runtimeWasUp,
|
|
804
908
|
};
|
|
@@ -858,14 +962,14 @@ async function studio(deps, args, options = {}) {
|
|
|
858
962
|
deps.out(`Studio ${login}`);
|
|
859
963
|
return 0;
|
|
860
964
|
}
|
|
861
|
-
const origin =
|
|
965
|
+
const origin = new URL(login).origin;
|
|
862
966
|
deps.out(`Studio ${next ? new URL(next, origin).toString() : origin}`);
|
|
863
967
|
await openLogin({ deps }, login);
|
|
864
968
|
return 0;
|
|
865
969
|
}
|
|
866
970
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
867
971
|
export async function runStudioCommand(args, deps, options = {}) {
|
|
868
|
-
await
|
|
972
|
+
await tidyMachine(deps);
|
|
869
973
|
return await studio(deps, args, options);
|
|
870
974
|
}
|
|
871
975
|
const COMMANDS = {
|
|
@@ -889,6 +993,6 @@ export async function runStackCommand(name, args, deps) {
|
|
|
889
993
|
const command = COMMANDS[name];
|
|
890
994
|
if (!command)
|
|
891
995
|
throw usageError(`Unknown command ${name}.\n${stackUsage}`);
|
|
892
|
-
await
|
|
996
|
+
await tidyMachine(deps);
|
|
893
997
|
return await command(deps, args);
|
|
894
998
|
}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* A local Tenant's Compose file (Runtime Architecture §14.3), written to
|
|
3
3
|
* `<Host root>/docker/compose.yaml` by `nylorun start`. Each local Tenant is one installation
|
|
4
4
|
* (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`), volumes,
|
|
5
|
-
* network and ports.
|
|
6
|
-
*
|
|
5
|
+
* network and ports. Container, network and volume names are global on the Docker engine, so
|
|
6
|
+
* every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
|
|
7
|
+
* `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
|
|
8
|
+
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
|
|
9
|
+
* UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
7
10
|
*
|
|
8
11
|
* The Tenant's state is its Postgres database, executed through Restate, with its
|
|
9
12
|
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
@@ -33,4 +36,4 @@
|
|
|
33
36
|
* it is down, model and MCP calls fail, deliveries are retried, vault writes and
|
|
34
37
|
* token minting answer 503, and the session takes the next message.
|
|
35
38
|
*/
|
|
36
|
-
export declare function renderComposeFile(project: string): string;
|
|
39
|
+
export declare function renderComposeFile(project: string, name: string): string;
|
|
@@ -3,8 +3,11 @@ import { PINNED_IMAGES } from "./images.js";
|
|
|
3
3
|
* A local Tenant's Compose file (Runtime Architecture §14.3), written to
|
|
4
4
|
* `<Host root>/docker/compose.yaml` by `nylorun start`. Each local Tenant is one installation
|
|
5
5
|
* (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`), volumes,
|
|
6
|
-
* network and ports.
|
|
7
|
-
*
|
|
6
|
+
* network and ports. Container, network and volume names are global on the Docker engine, so
|
|
7
|
+
* every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
|
|
8
|
+
* `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
|
|
9
|
+
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
|
|
10
|
+
* UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
8
11
|
*
|
|
9
12
|
* The Tenant's state is its Postgres database, executed through Restate, with its
|
|
10
13
|
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
@@ -34,13 +37,18 @@ import { PINNED_IMAGES } from "./images.js";
|
|
|
34
37
|
* it is down, model and MCP calls fail, deliveries are retried, vault writes and
|
|
35
38
|
* token minting answer 503, and the session takes the next message.
|
|
36
39
|
*/
|
|
37
|
-
export function renderComposeFile(project) {
|
|
40
|
+
export function renderComposeFile(project, name) {
|
|
38
41
|
return `# Written by \`nylorun start\`; rewritten on every start. Settings live in .env.
|
|
39
42
|
name: ${project}
|
|
40
43
|
|
|
44
|
+
x-tenant: &tenant
|
|
45
|
+
dev.nylorun.tenant: "${name}"
|
|
46
|
+
|
|
41
47
|
services:
|
|
42
48
|
postgres: # Session Store
|
|
43
49
|
image: ${PINNED_IMAGES.postgres}
|
|
50
|
+
container_name: ${project}-postgres
|
|
51
|
+
labels: *tenant
|
|
44
52
|
# Logical replication feeds the stream relay; a stuck slot is capped at 4 GB of WAL.
|
|
45
53
|
command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size=4GB"]
|
|
46
54
|
environment:
|
|
@@ -59,6 +67,8 @@ services:
|
|
|
59
67
|
|
|
60
68
|
restate: # Durable Session Execution
|
|
61
69
|
image: ${PINNED_IMAGES.restate}
|
|
70
|
+
container_name: ${project}-restate
|
|
71
|
+
labels: *tenant
|
|
62
72
|
command: ["--node-name=restate-1"] # stable name, so data is found on restart
|
|
63
73
|
environment:
|
|
64
74
|
RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
|
|
@@ -74,20 +84,24 @@ services:
|
|
|
74
84
|
retries: 30
|
|
75
85
|
restart: unless-stopped
|
|
76
86
|
|
|
77
|
-
s2: # Durable Streams; not published, only the Runtime reaches it
|
|
87
|
+
s2-lite: # Durable Streams; not published, only the Runtime reaches it
|
|
78
88
|
image: ${PINNED_IMAGES.s2}
|
|
89
|
+
container_name: ${project}-s2-lite
|
|
90
|
+
labels: *tenant
|
|
79
91
|
command: ["lite", "--local-root", "/home/nonroot/data"] # local disk; listens on port 80
|
|
80
92
|
# The image runs as uid 65532. Docker fills a new volume with the image's
|
|
81
93
|
# /home/nonroot, owned by that user; a volume at a path the image lacks
|
|
82
94
|
# (e.g. /data) is root-owned and s2-lite cannot write it.
|
|
83
95
|
volumes:
|
|
84
|
-
- s2:/home/nonroot
|
|
96
|
+
- s2-lite:/home/nonroot
|
|
85
97
|
# The s2 image has no shell or HTTP client, so it has no health check;
|
|
86
98
|
# the Runtime's /ready covers it.
|
|
87
99
|
restart: unless-stopped
|
|
88
100
|
|
|
89
101
|
gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published
|
|
90
102
|
image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
|
|
103
|
+
container_name: ${project}-gateway
|
|
104
|
+
labels: *tenant
|
|
91
105
|
command: ["--service", "gates,keys"]
|
|
92
106
|
user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
|
|
93
107
|
depends_on:
|
|
@@ -116,12 +130,14 @@ services:
|
|
|
116
130
|
|
|
117
131
|
runtime:
|
|
118
132
|
image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
|
|
133
|
+
container_name: ${project}-runtime
|
|
134
|
+
labels: *tenant
|
|
119
135
|
command: ["--service", "core,loop"]
|
|
120
136
|
user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
|
|
121
137
|
depends_on:
|
|
122
138
|
postgres: { condition: service_healthy }
|
|
123
139
|
restate: { condition: service_healthy }
|
|
124
|
-
s2: { condition: service_started }
|
|
140
|
+
s2-lite: { condition: service_started }
|
|
125
141
|
environment:
|
|
126
142
|
NYLORUN_HOME: /nylorun
|
|
127
143
|
NYLORUN_PACKING: combined
|
|
@@ -149,7 +165,7 @@ services:
|
|
|
149
165
|
NYLORUN_RESTATE_ADMIN_URL: http://restate:9070
|
|
150
166
|
NYLORUN_WORKER_URL: http://runtime:9080 # registered with Restate; not published
|
|
151
167
|
NYLORUN_RESTATE_IDENTITY_KEY: \${NYLORUN_RESTATE_IDENTITY_KEY:?run nylorun start}
|
|
152
|
-
NYLORUN_S2_ENDPOINT: http://s2:80
|
|
168
|
+
NYLORUN_S2_ENDPOINT: http://s2-lite:80
|
|
153
169
|
NYLORUN_S2_TOKEN: ignored # s2-lite has no access tokens yet
|
|
154
170
|
NYLORUN_WORKSPACE_STORE_URL: file:///workspaces
|
|
155
171
|
# Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
|
|
@@ -181,6 +197,8 @@ services:
|
|
|
181
197
|
|
|
182
198
|
studio: # dashboard + trusted proxy
|
|
183
199
|
image: \${NYLORUN_STUDIO_IMAGE:?run nylorun start}
|
|
200
|
+
container_name: ${project}-studio
|
|
201
|
+
labels: *tenant
|
|
184
202
|
user: "\${NYLORUN_UID}:\${NYLORUN_GID}" # reads the 0600 admin key file
|
|
185
203
|
depends_on:
|
|
186
204
|
runtime: { condition: service_healthy }
|
|
@@ -190,6 +208,9 @@ services:
|
|
|
190
208
|
PORT: "3000"
|
|
191
209
|
# Studio's Host check accepts localhost/127.0.0.1 on the published port.
|
|
192
210
|
NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
|
|
211
|
+
# Browsers share cookies across ports of one host: a cookie per Tenant keeps the
|
|
212
|
+
# sessions of two Studios on localhost apart.
|
|
213
|
+
NYLORUN_STUDIO_SESSION_COOKIE: nylorun_studio_${name}
|
|
193
214
|
# Exact origins that may frame Studio (Babai Desktop); kept in .env.
|
|
194
215
|
NYLORUN_STUDIO_FRAME_ANCESTORS: \${NYLORUN_STUDIO_FRAME_ANCESTORS:-}
|
|
195
216
|
# Studio's anonymous usage analytics; empty when telemetry is off.
|
|
@@ -205,10 +226,15 @@ services:
|
|
|
205
226
|
retries: 30
|
|
206
227
|
restart: unless-stopped
|
|
207
228
|
|
|
229
|
+
networks:
|
|
230
|
+
default:
|
|
231
|
+
name: ${project}
|
|
232
|
+
labels: *tenant
|
|
233
|
+
|
|
208
234
|
volumes:
|
|
209
|
-
postgres: {}
|
|
210
|
-
restate: {}
|
|
211
|
-
s2: {}
|
|
212
|
-
workspaces: {}
|
|
235
|
+
postgres: { name: ${project}-postgres, labels: *tenant }
|
|
236
|
+
restate: { name: ${project}-restate, labels: *tenant }
|
|
237
|
+
s2-lite: { name: ${project}-s2-lite, labels: *tenant }
|
|
238
|
+
workspaces: { name: ${project}-workspaces, labels: *tenant }
|
|
213
239
|
`;
|
|
214
240
|
}
|
package/dist/stack/docker.d.ts
CHANGED
|
@@ -36,3 +36,13 @@ export interface ComposeService {
|
|
|
36
36
|
}
|
|
37
37
|
/** Parse `docker compose ps --format json` (a JSON array, or one object per line). */
|
|
38
38
|
export declare function parseComposePs(stdout: string): ComposeService[];
|
|
39
|
+
/** `123.4MiB` (from `docker stats` MemUsage) in bytes; undefined when unreadable. */
|
|
40
|
+
export declare function parseByteSize(text: string): number | undefined;
|
|
41
|
+
/** `812 MB`, `2.1 GB`. */
|
|
42
|
+
export declare function formatBytes(bytes: number): string;
|
|
43
|
+
/**
|
|
44
|
+
* Memory in use per Tenant: the sum over its running containers, found by the label
|
|
45
|
+
* `dev.nylorun.tenant`, from one `docker stats --no-stream`. Undefined when Docker does not
|
|
46
|
+
* answer; a Tenant with no running container is absent.
|
|
47
|
+
*/
|
|
48
|
+
export declare function tenantMemory(docker: DockerRunner): Promise<Map<string, number> | undefined>;
|
package/dist/stack/docker.js
CHANGED
|
@@ -99,3 +99,66 @@ export function parseComposePs(stdout) {
|
|
|
99
99
|
};
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
const UNITS = {
|
|
103
|
+
b: 1,
|
|
104
|
+
kb: 1e3,
|
|
105
|
+
mb: 1e6,
|
|
106
|
+
gb: 1e9,
|
|
107
|
+
tb: 1e12,
|
|
108
|
+
kib: 2 ** 10,
|
|
109
|
+
mib: 2 ** 20,
|
|
110
|
+
gib: 2 ** 30,
|
|
111
|
+
tib: 2 ** 40,
|
|
112
|
+
};
|
|
113
|
+
/** `123.4MiB` (from `docker stats` MemUsage) in bytes; undefined when unreadable. */
|
|
114
|
+
export function parseByteSize(text) {
|
|
115
|
+
const match = /^([\d.]+)\s*([a-z]+)$/i.exec(text.trim());
|
|
116
|
+
const unit = match ? UNITS[match[2].toLowerCase()] : undefined;
|
|
117
|
+
return match && unit ? Number(match[1]) * unit : undefined;
|
|
118
|
+
}
|
|
119
|
+
/** `812 MB`, `2.1 GB`. */
|
|
120
|
+
export function formatBytes(bytes) {
|
|
121
|
+
return bytes >= 1e9 ? `${(bytes / 1e9).toFixed(1)} GB` : `${Math.round(bytes / 1e6)} MB`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Memory in use per Tenant: the sum over its running containers, found by the label
|
|
125
|
+
* `dev.nylorun.tenant`, from one `docker stats --no-stream`. Undefined when Docker does not
|
|
126
|
+
* answer; a Tenant with no running container is absent.
|
|
127
|
+
*/
|
|
128
|
+
export async function tenantMemory(docker) {
|
|
129
|
+
const ps = await docker.run([
|
|
130
|
+
"ps",
|
|
131
|
+
"--filter",
|
|
132
|
+
"label=dev.nylorun.tenant",
|
|
133
|
+
"--format",
|
|
134
|
+
'{{.Names}}\t{{.Label "dev.nylorun.tenant"}}',
|
|
135
|
+
]);
|
|
136
|
+
if (ps.code !== 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
const owners = new Map(ps.stdout
|
|
139
|
+
.split(/\r?\n/)
|
|
140
|
+
.map((line) => line.trim().split("\t"))
|
|
141
|
+
.filter((cells) => cells.length === 2 && cells[1] !== ""));
|
|
142
|
+
const memory = new Map();
|
|
143
|
+
if (owners.size === 0)
|
|
144
|
+
return memory;
|
|
145
|
+
const stats = await docker.run([
|
|
146
|
+
"stats",
|
|
147
|
+
"--no-stream",
|
|
148
|
+
"--format",
|
|
149
|
+
"{{.Name}}\t{{.MemUsage}}",
|
|
150
|
+
...owners.keys(),
|
|
151
|
+
]);
|
|
152
|
+
if (stats.code !== 0)
|
|
153
|
+
return undefined;
|
|
154
|
+
for (const line of stats.stdout.split(/\r?\n/)) {
|
|
155
|
+
const [name, usage] = line.trim().split("\t");
|
|
156
|
+
const tenant = name ? owners.get(name) : undefined;
|
|
157
|
+
const bytes = usage ? parseByteSize(usage.split("/")[0]) : undefined;
|
|
158
|
+
if (tenant && bytes !== undefined)
|
|
159
|
+
memory.set(tenant, (memory.get(tenant) ?? 0) + bytes);
|
|
160
|
+
}
|
|
161
|
+
for (const [tenant, bytes] of memory)
|
|
162
|
+
memory.set(tenant, Math.round(bytes));
|
|
163
|
+
return memory;
|
|
164
|
+
}
|
package/dist/stack/prepare.js
CHANGED
|
@@ -90,6 +90,6 @@ export async function prepareStack(input) {
|
|
|
90
90
|
runtimeVersion: input.runtimeVersion,
|
|
91
91
|
});
|
|
92
92
|
await writeFileMode(paths.env, renderEnvFile(env), 0o600);
|
|
93
|
-
await writeFileMode(paths.compose, renderComposeFile(input.project), 0o644);
|
|
93
|
+
await writeFileMode(paths.compose, renderComposeFile(input.project, input.name), 0o644);
|
|
94
94
|
return { env, host, adminKey, firstRun };
|
|
95
95
|
}
|
package/dist/stack/stacks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DockerRunner } from "./docker.js";
|
|
1
2
|
/**
|
|
2
3
|
* The machine's local Tenants (tenancy.md §6): one installation per Tenant, each with its own
|
|
3
4
|
* Host root `~/.nylorun/tenants/<name>/`, Compose project `nylorun-<name>`, ports and volumes.
|
|
@@ -48,3 +49,10 @@ export declare function chooseTenantName(base: string, projectDir: string): Prom
|
|
|
48
49
|
* volumes with new keys.
|
|
49
50
|
*/
|
|
50
51
|
export declare function moveStackRoots(base: string, err: (line: string) => void): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
|
|
54
|
+
* files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
|
|
55
|
+
* networks, so their `docker compose down` works again. The directory goes last, once Docker
|
|
56
|
+
* removed both (or never had them), so a Docker that does not answer is retried next time.
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeOldProxy(docker: DockerRunner, base: string, err: (line: string) => void): Promise<void>;
|
package/dist/stack/stacks.js
CHANGED
|
@@ -148,3 +148,22 @@ export async function moveStackRoots(base, err) {
|
|
|
148
148
|
err(`Moved Tenants ${moved.join(", ")} from ${tilde(stacks)} to ${tilde(tenantsDir(base))}.`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Remove the Studio proxy of nylorun 0.6 (a Caddy container `nylorun-proxy` on its own network,
|
|
153
|
+
* files in `~/.nylorun/proxy/`). Removing the container also detaches it from the Tenants'
|
|
154
|
+
* networks, so their `docker compose down` works again. The directory goes last, once Docker
|
|
155
|
+
* removed both (or never had them), so a Docker that does not answer is retried next time.
|
|
156
|
+
*/
|
|
157
|
+
export async function removeOldProxy(docker, base, err) {
|
|
158
|
+
const dir = join(base, "proxy");
|
|
159
|
+
if (!existsSync(dir))
|
|
160
|
+
return;
|
|
161
|
+
const container = await docker.run(["rm", "--force", "nylorun-proxy"]);
|
|
162
|
+
if (container.code !== 0 && !/no such container/i.test(container.stderr))
|
|
163
|
+
return;
|
|
164
|
+
const network = await docker.run(["network", "rm", "nylorun-proxy"]);
|
|
165
|
+
if (network.code !== 0 && !/not found|no such network/i.test(network.stderr))
|
|
166
|
+
return;
|
|
167
|
+
await rm(dir, { recursive: true, force: true });
|
|
168
|
+
err("Removed the Studio proxy of nylorun 0.6 (nylorun-proxy); each Studio is on its own localhost port again (\"nylorun studio\" opens it).");
|
|
169
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
2
|
-
/** Studio's origin
|
|
2
|
+
/** Studio's published origin: always `localhost` (the cookie's host). */
|
|
3
3
|
export declare function studioOrigin(port: number): string;
|
|
4
4
|
/**
|
|
5
5
|
* Ask the Studio container for a single-use login token with the admin key
|
|
6
|
-
* (`POST /_studio/login-tokens`, Studio §6)
|
|
6
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
7
|
+
* and return the URL to open there.
|
|
7
8
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
8
9
|
*/
|
|
9
10
|
export declare function mintStudioLogin(input: {
|
|
10
11
|
fetch: FetchLike;
|
|
11
|
-
|
|
12
|
+
origin: string;
|
|
12
13
|
adminKey: string;
|
|
13
14
|
timeoutMs?: number;
|
|
14
15
|
}): Promise<string>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { STACK_CLIENT_HOST } from "./host-files.js";
|
|
2
|
-
/** Studio's origin
|
|
2
|
+
/** Studio's published origin: always `localhost` (the cookie's host). */
|
|
3
3
|
export function studioOrigin(port) {
|
|
4
4
|
return `http://${STACK_CLIENT_HOST}:${port}`;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Ask the Studio container for a single-use login token with the admin key
|
|
8
|
-
* (`POST /_studio/login-tokens`, Studio §6)
|
|
8
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
9
|
+
* and return the URL to open there.
|
|
9
10
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
10
11
|
*/
|
|
11
12
|
export async function mintStudioLogin(input) {
|
|
12
|
-
const origin =
|
|
13
|
+
const { origin } = input;
|
|
13
14
|
const response = await input.fetch(`${origin}/_studio/login-tokens`, {
|
|
14
15
|
method: "POST",
|
|
15
16
|
headers: {
|
|
@@ -27,7 +28,7 @@ export async function mintStudioLogin(input) {
|
|
|
27
28
|
for (const field of ["loginUrl", "url"]) {
|
|
28
29
|
const value = body[field];
|
|
29
30
|
if (typeof value === "string" && value !== "") {
|
|
30
|
-
// A relative URL is resolved against the
|
|
31
|
+
// A relative URL is resolved against the origin it was minted on.
|
|
31
32
|
return new URL(value, origin).toString();
|
|
32
33
|
}
|
|
33
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nylorun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"nylorun": "dist/cli.js"
|
|
42
42
|
},
|
|
43
43
|
"nylorun": {
|
|
44
|
-
"runtime": "0.15.
|
|
45
|
-
"studio": "0.
|
|
44
|
+
"runtime": "0.15.2-beta",
|
|
45
|
+
"studio": "0.18.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|