nylorun 0.6.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 +10 -0
- package/README.md +26 -62
- package/dist/doctor.d.ts +1 -3
- package/dist/doctor.js +2 -19
- package/dist/stack/commands.d.ts +3 -8
- package/dist/stack/commands.js +26 -100
- package/dist/stack/compose-file.d.ts +1 -1
- package/dist/stack/compose-file.js +2 -7
- package/dist/stack/env-file.d.ts +0 -5
- package/dist/stack/env-file.js +0 -5
- package/dist/stack/images.d.ts +1 -3
- package/dist/stack/images.js +1 -2
- package/dist/stack/index.d.ts +1 -2
- package/dist/stack/index.js +1 -3
- package/dist/stack/ports.d.ts +0 -2
- package/dist/stack/ports.js +0 -2
- package/dist/stack/prepare.d.ts +0 -2
- package/dist/stack/prepare.js +0 -1
- package/dist/stack/stacks.d.ts +8 -0
- package/dist/stack/stacks.js +19 -0
- package/dist/stack/studio-login.d.ts +2 -2
- package/dist/stack/studio-login.js +2 -2
- package/package.json +2 -2
- package/dist/stack/proxy.d.ts +0 -78
- package/dist/stack/proxy.js +0 -224
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.6.0-beta
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ 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> | --all] # stop the containers (--all: every Tenant's
|
|
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
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
|
|
@@ -82,29 +82,29 @@ projects and volumes.
|
|
|
82
82
|
|
|
83
83
|
Tenants run side by side, each on its own ports, containers, network and
|
|
84
84
|
volumes ([The containers](#the-containers)); a running Tenant uses about
|
|
85
|
-
|
|
86
|
-
|
|
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`
|
|
87
88
|
lists them with their state, the memory their containers use (`MEMORY`: the sum
|
|
88
89
|
of `docker stats` over the containers labelled `dev.nylorun.tenant=<name>`, `-`
|
|
89
90
|
when stopped; `memoryBytes` in `--json`, `null` when stopped or unknown), the
|
|
90
91
|
Runtime's URL and Studio's:
|
|
91
92
|
|
|
92
93
|
```text
|
|
93
|
-
TENANT STATE MEMORY RUNTIME STUDIO
|
|
94
|
-
api running
|
|
95
|
-
default stopped - http://localhost:8787 http://
|
|
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 -
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
When other Tenants are running, `start` says so after its summary, on stderr
|
|
99
100
|
(without the size when Docker does not report it):
|
|
100
101
|
|
|
101
102
|
```text
|
|
102
|
-
Also running: default, api (about
|
|
103
|
+
Also running: default, api (about 2.4 GB). "nylorun stop --all" stops them all.
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
`nylorun stop --all` stops every running Tenant on this machine
|
|
106
|
-
|
|
107
|
-
`stop` of one Tenant leaves the proxy running.
|
|
106
|
+
`nylorun stop --all` stops every running Tenant on this machine, keeping their
|
|
107
|
+
volumes; it cannot be combined with `--tenant` (exit 2).
|
|
108
108
|
|
|
109
109
|
A Tenant created by nylorun 0.5 keeps its data in volumes Compose named
|
|
110
110
|
`<project>_postgres`, `<project>_restate`, `<project>_s2` and
|
|
@@ -167,67 +167,31 @@ project, and each has the label `dev.nylorun.tenant: <name>`. For Tenant `shop`:
|
|
|
167
167
|
| Network | `nylorun-shop` |
|
|
168
168
|
| Volumes | `nylorun-shop-postgres`, `nylorun-shop-restate`, `nylorun-shop-s2-lite`, `nylorun-shop-workspaces` |
|
|
169
169
|
|
|
170
|
-
Postgres initialises the Tenant's database with C collation. Restate
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
600–700 MB, down from about 1.3 GB. The Runtime and Studio images are pinned by
|
|
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
|
|
174
173
|
this release (`package.json` `nylorun.runtime` and `nylorun.studio`);
|
|
175
174
|
`NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them (local builds,
|
|
176
175
|
CI). Ports publish on loopback only: the Runtime on `8787`, its operator
|
|
177
176
|
listener (Admin API) on `8788`, Studio on `4161` and the Restate UI on `9070`,
|
|
178
177
|
or free ports chosen on the Tenant's first start, avoiding the ports other
|
|
179
|
-
Tenants
|
|
178
|
+
Tenants keep, and kept in its `.env`.
|
|
180
179
|
|
|
181
|
-
`start`, `ls` and `nylorun studio` give Studio as
|
|
182
|
-
|
|
183
|
-
`http://localhost:<port>` (`--json` has both: `studio.url` is Studio's own port,
|
|
184
|
-
which an embedding app frames, and `studio.proxyUrl` the proxy's). Studio has no
|
|
180
|
+
`start`, `status`, `ls` and `nylorun studio` give Studio as
|
|
181
|
+
`http://localhost:<port>` (`studio.url` in `status --json`). Studio has no
|
|
185
182
|
password: in a terminal (not in CI, and not with `--no-open`), `start` asks the
|
|
186
183
|
Studio container for a single-use login token with the admin key (`POST
|
|
187
|
-
/_studio/login-tokens
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`nylorun studio`, which signs a browser in the same way; `nylorun studio
|
|
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
|
|
193
189
|
--no-open` prints the login URL (it works once, for two minutes) instead.
|
|
194
190
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
One small Caddy container per machine (`nylorun-proxy`, Compose project
|
|
201
|
-
`nylorun-proxy`, label `dev.nylorun.proxy: "true"`) gives every Tenant's Studio
|
|
202
|
-
the address `http://<name>.localhost:<port>`. Browsers keep cookies per host,
|
|
203
|
-
not per port, so separate hosts keep the Studios' sessions apart. The proxy is
|
|
204
|
-
for browsers only: programs keep using the Runtime's `http://localhost:<port>`.
|
|
205
|
-
It holds no Tenant data; its files are in `~/.nylorun/proxy/` (`compose.yaml`,
|
|
206
|
-
`Caddyfile`, and `.env` with `NYLORUN_PROXY_PORT`, 4160 or a free port chosen
|
|
207
|
-
on its first start and kept; Caddy's own state is tmpfs). Its image is pinned
|
|
208
|
-
by this release (`caddy:2.11.6`).
|
|
209
|
-
|
|
210
|
-
- Caddy listens on port 80 in the container, published at `NYLORUN_PROXY_PORT`
|
|
211
|
-
on `127.0.0.1` and `[::1]` (macOS resolves `*.localhost` to `::1` only); when
|
|
212
|
-
Docker refuses `::1` (IPv6 off), it publishes on `127.0.0.1` alone and says
|
|
213
|
-
so once.
|
|
214
|
-
- It routes every Tenant under `~/.nylorun/tenants/`: it joins each Tenant's
|
|
215
|
-
network and reaches `<project>-studio:3000`, passing the browser's `Host`
|
|
216
|
-
through, which Studio checks. A stopped Tenant answers 502 naming
|
|
217
|
-
`nylorun start --tenant <name>`; an unknown host 404 naming `nylorun ls`.
|
|
218
|
-
- `start` rewrites its files, brings it up (only when Studio starts: not with
|
|
219
|
-
`--no-studio`), joins every Tenant's network and reloads Caddy; Studio learns
|
|
220
|
-
its proxy origin from `NYLORUN_STUDIO_PUBLIC_ORIGINS`, written to the
|
|
221
|
-
Tenant's `.env` on every start. A proxy that does not come up never fails
|
|
222
|
-
`start`: it warns once and prints Studio's own URL.
|
|
223
|
-
- `reset` and `delete` detach it from the Tenant's network first; `delete`
|
|
224
|
-
removes the Tenant's route. `stop --all` stops it.
|
|
225
|
-
- `NYLORUN_PROXY_DISABLED=1` turns it off: Studio is `http://localhost:<port>`.
|
|
226
|
-
A Tenant under `NYLORUN_HOME` or `NYLORUN_COMPOSE_PROJECT` (the repository's
|
|
227
|
-
smokes) does not use it either.
|
|
228
|
-
|
|
229
|
-
`nylorun doctor` reports it on a `proxy` row: running (on which loopbacks and
|
|
230
|
-
port), stopped, not created yet, or disabled.
|
|
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.
|
|
231
195
|
|
|
232
196
|
### Embedding Studio in a desktop app
|
|
233
197
|
|
|
@@ -308,7 +272,7 @@ containers, volumes and Host root: the vault key (KEK) and all its data go with
|
|
|
308
272
|
| Port conflict | Change `NYLORUN_PORT` / `NYLORUN_STUDIO_PORT` in `~/.nylorun/tenants/<name>/docker/.env` |
|
|
309
273
|
| Logs | `nylorun logs runtime -f` |
|
|
310
274
|
| Studio login expired | `nylorun studio` |
|
|
311
|
-
|
|
|
275
|
+
| Which Studio is which | `nylorun ls` lists each Tenant's Studio URL; `nylorun studio --tenant <name>` opens one signed in |
|
|
312
276
|
| `Tenant … was created by nylorun 0.5` (exit 3) | `nylorun reset --tenant <name>` (its data starts fresh) |
|
|
313
277
|
|
|
314
278
|
See [MIGRATION.md](../MIGRATION.md).
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Check, type
|
|
1
|
+
import { type Check, type StackDeps, type StackStatus } from "./stack/index.js";
|
|
2
2
|
export interface StackDoctorReport {
|
|
3
3
|
node: {
|
|
4
4
|
version: string;
|
|
@@ -9,8 +9,6 @@ export interface StackDoctorReport {
|
|
|
9
9
|
tenant?: Pick<StackStatus, "name" | "project" | "home" | "state" | "runtime" | "tenant" | "studio" | "gateway">;
|
|
10
10
|
/** Why no Tenant was checked: none is selected here (a project without a link). */
|
|
11
11
|
noTenant?: string;
|
|
12
|
-
/** The machine's Studio proxy (`http://<tenant>.localhost:<port>`); never a failure. */
|
|
13
|
-
proxy?: ProxyStatus;
|
|
14
12
|
}
|
|
15
13
|
/**
|
|
16
14
|
* `nylorun doctor`: check the prerequisites of a local Tenant (Node 24+,
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
|
-
import { checkDocker, defaultStackDeps,
|
|
2
|
+
import { checkDocker, defaultStackDeps, readStackStatus, } from "./stack/index.js";
|
|
3
3
|
/** Oldest Node nylorun and the developer's application run on. */
|
|
4
4
|
const MIN_NODE_MAJOR = 24;
|
|
5
5
|
/**
|
|
@@ -36,7 +36,6 @@ export async function doctorStack(options) {
|
|
|
36
36
|
throw error;
|
|
37
37
|
report.noTenant = error.message;
|
|
38
38
|
}
|
|
39
|
-
report.proxy = await readProxyReport(deps);
|
|
40
39
|
}
|
|
41
40
|
const broken = report.tenant?.state === "running" &&
|
|
42
41
|
(!report.tenant.runtime.healthy ||
|
|
@@ -96,25 +95,9 @@ export async function doctorStack(options) {
|
|
|
96
95
|
? `✓ ${local.tenant.id ?? "?"} open`
|
|
97
96
|
: `✗ ${local.tenant.id ?? "?"} unavailable${local.tenant.cause ? `: ${local.tenant.cause.code}: ${local.tenant.cause.repair}` : " (opening)"}`,
|
|
98
97
|
]);
|
|
99
|
-
rows.push(["studio", `${local.studio.
|
|
98
|
+
rows.push(["studio", `${local.studio.url ?? "?"} · ${local.studio.state}`]);
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
|
-
const proxy = report.proxy;
|
|
103
|
-
if (proxy) {
|
|
104
|
-
const on = proxy.port
|
|
105
|
-
? `127.0.0.1:${proxy.port}${proxy.ipv6 ? ` and [::1]:${proxy.port}` : ""}`
|
|
106
|
-
: "";
|
|
107
|
-
rows.push([
|
|
108
|
-
"proxy",
|
|
109
|
-
proxy.state === "running"
|
|
110
|
-
? `✓ running on ${on} (http://<tenant>.localhost:${proxy.port ?? "?"})`
|
|
111
|
-
: proxy.state === "disabled"
|
|
112
|
-
? "- disabled (NYLORUN_PROXY_DISABLED): Studio is on each Tenant's own port"
|
|
113
|
-
: proxy.state === "stopped"
|
|
114
|
-
? `- stopped${on ? ` (${on})` : ""}: nylorun start starts it`
|
|
115
|
-
: "- not created yet: nylorun start creates it",
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
101
|
const width = Math.max(...rows.map(([key]) => key.length)) + 2;
|
|
119
102
|
for (const [key, value] of rows)
|
|
120
103
|
log(` ${key.padEnd(width)}${value}`);
|
package/dist/stack/commands.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
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
|
-
import { type ProxyStatus } from "./proxy.js";
|
|
5
4
|
import { type FetchLike } from "./studio-login.js";
|
|
6
5
|
export declare const STACK_SERVICES: readonly ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
|
|
7
6
|
export declare const stackUsage: string;
|
|
@@ -74,10 +73,8 @@ export interface StackStatus {
|
|
|
74
73
|
/** The Tenant as the Runtime reports it, while it answers. */
|
|
75
74
|
tenant?: StackTenant;
|
|
76
75
|
studio: {
|
|
77
|
-
/**
|
|
76
|
+
/** `http://localhost:<port>`: also what an embedding app frames (same site as its own localhost). */
|
|
78
77
|
url?: string;
|
|
79
|
-
/** Through the Studio proxy, when it serves the Tenant. */
|
|
80
|
-
proxyUrl?: string;
|
|
81
78
|
state: string;
|
|
82
79
|
/** Exact origins that may show Studio in a frame (Studio §8.9). */
|
|
83
80
|
embedOrigins?: string[];
|
|
@@ -92,8 +89,6 @@ export interface StackStatus {
|
|
|
92
89
|
};
|
|
93
90
|
services: ComposeService[];
|
|
94
91
|
}
|
|
95
|
-
/** The Studio proxy, as data (`nylorun doctor`). */
|
|
96
|
-
export declare function readProxyReport(deps: StackDeps): Promise<ProxyStatus>;
|
|
97
92
|
/** What `nylorun status` reports, as data (`nylorun doctor`). */
|
|
98
93
|
export declare function readStackStatus(deps: StackDeps, options?: {
|
|
99
94
|
name?: string;
|
|
@@ -111,7 +106,7 @@ export interface StackEndpoints {
|
|
|
111
106
|
hostId: string;
|
|
112
107
|
adminKey: string;
|
|
113
108
|
studioPort: number;
|
|
114
|
-
/**
|
|
109
|
+
/** `http://localhost:<studio port>` */
|
|
115
110
|
studioUrl: string;
|
|
116
111
|
/** Studio is running and healthy. */
|
|
117
112
|
studioUp: boolean;
|
|
@@ -135,7 +130,7 @@ export declare function tenantStudioPath(tenantId: string): string;
|
|
|
135
130
|
* Mint a fresh Studio login URL on a running Tenant, landing on `next`.
|
|
136
131
|
* Undefined (after a warning) when Studio does not answer.
|
|
137
132
|
*/
|
|
138
|
-
export declare function studioLoginUrl(deps: StackDeps, running: Pick<StackEndpoints, "studioPort" | "
|
|
133
|
+
export declare function studioLoginUrl(deps: StackDeps, running: Pick<StackEndpoints, "studioPort" | "adminKey">, next?: string): Promise<string | undefined>;
|
|
139
134
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
140
135
|
export declare function runStudioCommand(args: readonly string[], deps: StackDeps, options?: {
|
|
141
136
|
next?: string;
|
package/dist/stack/commands.js
CHANGED
|
@@ -13,8 +13,7 @@ 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 {
|
|
17
|
-
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";
|
|
18
17
|
import { mintStudioLogin, studioOrigin } from "./studio-login.js";
|
|
19
18
|
export const STACK_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime", "studio"];
|
|
20
19
|
const CORE_SERVICES = ["postgres", "restate", "s2-lite", "gateway", "runtime"];
|
|
@@ -23,7 +22,7 @@ export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio
|
|
|
23
22
|
credentials.json) on the first run; print the Runtime and Studio URLs and open Studio
|
|
24
23
|
signed in (in a terminal). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
25
24
|
starts the default Tenant (or --tenant's) without linking the current directory
|
|
26
|
-
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's
|
|
25
|
+
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
|
|
27
26
|
status [--tenant <name>] [--json] the Tenant, its id, services, endpoints and Runtime health
|
|
28
27
|
logs [service] [--tenant <name>] [-f] [--tail <n>]
|
|
29
28
|
container logs (${STACK_SERVICES.join(", ")})
|
|
@@ -43,26 +42,11 @@ export function stackProject(env, name) {
|
|
|
43
42
|
function nylorunRoot(deps) {
|
|
44
43
|
return deps.nylorunRoot ?? defaultNylorunRoot();
|
|
45
44
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function proxied(ctx) {
|
|
52
|
-
const { env } = ctx.deps;
|
|
53
|
-
return !proxyDisabled(env) && !env.NYLORUN_HOME?.trim() && !env.NYLORUN_COMPOSE_PROJECT?.trim();
|
|
54
|
-
}
|
|
55
|
-
/** Studio's URL for browsers: through the proxy when it serves the Tenant, else its own port. */
|
|
56
|
-
async function studioUrl(ctx, studioPort) {
|
|
57
|
-
const settings = proxied(ctx) ? await readProxySettings(nylorunRoot(ctx.deps)) : undefined;
|
|
58
|
-
return settings ? proxyOrigin(ctx.name, settings.port) : studioOrigin(studioPort);
|
|
59
|
-
}
|
|
60
|
-
/** The proxy's routes: every Tenant on this machine. */
|
|
61
|
-
async function proxyRoutes(deps) {
|
|
62
|
-
return (await listTenants(nylorunRoot(deps))).map((tenant) => ({
|
|
63
|
-
name: tenant.name,
|
|
64
|
-
project: stackProject(deps.env, tenant.name),
|
|
65
|
-
}));
|
|
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);
|
|
66
50
|
}
|
|
67
51
|
/**
|
|
68
52
|
* Compose's names for a 0.5 Tenant's volumes (`<project>_postgres`, …) that exist on this
|
|
@@ -78,13 +62,9 @@ async function oldVolumes(docker, project) {
|
|
|
78
62
|
current: names.has(`${project}-postgres`),
|
|
79
63
|
};
|
|
80
64
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Before `docker compose down` (`reset`, `delete`): detach the proxy from the Tenant's network
|
|
83
|
-
* so Compose can remove it. After it: drop a 0.5 Tenant's volumes and network.
|
|
84
|
-
*/
|
|
65
|
+
/** `docker compose down` (`reset`, `delete`), then drop a 0.5 Tenant's volumes and network. */
|
|
85
66
|
async function composeDown(ctx, args) {
|
|
86
67
|
const { docker } = ctx.deps;
|
|
87
|
-
await disconnectProxy(docker, ctx.project);
|
|
88
68
|
const code = await docker.stream(args);
|
|
89
69
|
if (code !== 0)
|
|
90
70
|
throw new CliError(`docker compose down failed (exit ${code}).`, 1);
|
|
@@ -356,14 +336,9 @@ async function tenantPorts(base, except) {
|
|
|
356
336
|
}
|
|
357
337
|
return reserved;
|
|
358
338
|
}
|
|
359
|
-
/** Ports other Tenants
|
|
339
|
+
/** Ports other Tenants keep, so a new Tenant avoids them. */
|
|
360
340
|
async function reservedPorts(ctx) {
|
|
361
|
-
|
|
362
|
-
const reserved = await tenantPorts(base, ctx.paths.root);
|
|
363
|
-
const proxy = await readProxySettings(base);
|
|
364
|
-
if (proxy)
|
|
365
|
-
reserved.add(proxy.port);
|
|
366
|
-
return reserved;
|
|
341
|
+
return await tenantPorts(nylorunRoot(ctx.deps), ctx.paths.root);
|
|
367
342
|
}
|
|
368
343
|
async function bringUp(ctx, options) {
|
|
369
344
|
const { deps } = ctx;
|
|
@@ -377,10 +352,6 @@ async function bringUp(ctx, options) {
|
|
|
377
352
|
const base = nylorunRoot(deps);
|
|
378
353
|
const telemetry = await readTelemetry(base);
|
|
379
354
|
const analytics = telemetryDecision(deps.env, telemetry).enabled;
|
|
380
|
-
// The proxy's port avoids every Tenant's; a new Tenant's ports then avoid the proxy's.
|
|
381
|
-
const proxy = proxied(ctx)
|
|
382
|
-
? await ensureProxySettings(base, deps.ports, await tenantPorts(base))
|
|
383
|
-
: undefined;
|
|
384
355
|
const prepared = await prepareStack({
|
|
385
356
|
paths: ctx.paths,
|
|
386
357
|
name: ctx.name,
|
|
@@ -400,7 +371,6 @@ async function bringUp(ctx, options) {
|
|
|
400
371
|
: {}),
|
|
401
372
|
...(options.studioEmbedOrigins ? { studioEmbedOrigins: options.studioEmbedOrigins } : {}),
|
|
402
373
|
...(analytics ? { studioAnalyticsId: STUDIO_ANALYTICS_ID } : {}),
|
|
403
|
-
...(proxy ? { studioPublicOrigin: proxyOrigin(ctx.name, proxy.port) } : {}),
|
|
404
374
|
});
|
|
405
375
|
if (analytics && options.studio && telemetry.noticeShown === undefined) {
|
|
406
376
|
deps.err(TELEMETRY_NOTICE);
|
|
@@ -430,45 +400,21 @@ async function bringUp(ctx, options) {
|
|
|
430
400
|
if (!studioStarted)
|
|
431
401
|
deps.err(`Warning: Studio did not start (image ${prepared.env.studioImage}). The Runtime is up; see "nylorun logs studio".`);
|
|
432
402
|
}
|
|
433
|
-
let studioUrl = studioOrigin(prepared.env.studioPort);
|
|
434
|
-
if (studioStarted && proxy) {
|
|
435
|
-
const up = await startProxy({
|
|
436
|
-
docker: deps.docker,
|
|
437
|
-
base,
|
|
438
|
-
settings: proxy,
|
|
439
|
-
routes: await proxyRoutes(deps),
|
|
440
|
-
err: deps.err,
|
|
441
|
-
...(deps.pollMs !== undefined ? { pollMs: deps.pollMs } : {}),
|
|
442
|
-
});
|
|
443
|
-
if (up)
|
|
444
|
-
studioUrl = proxyOrigin(ctx.name, proxy.port);
|
|
445
|
-
}
|
|
446
403
|
return {
|
|
447
404
|
runtimeUrl,
|
|
448
405
|
adminUrl,
|
|
449
406
|
hostId: prepared.host.hostId,
|
|
450
407
|
studioPort: prepared.env.studioPort,
|
|
451
|
-
studioUrl,
|
|
408
|
+
studioUrl: studioOrigin(prepared.env.studioPort),
|
|
452
409
|
studioStarted,
|
|
453
410
|
adminKey: prepared.adminKey,
|
|
454
411
|
};
|
|
455
412
|
}
|
|
456
413
|
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
* does not answer falls back, saying so), else on Studio's own port, retried while it starts.
|
|
460
|
-
*/
|
|
461
|
-
async function tryStudioLogin(ctx, studio, adminKey) {
|
|
414
|
+
/** A login URL on Studio's own port, retried while it starts. */
|
|
415
|
+
async function tryStudioLogin(ctx, studioPort, adminKey) {
|
|
462
416
|
const { deps } = ctx;
|
|
463
|
-
const origin = studioOrigin(
|
|
464
|
-
if (studio.url !== origin) {
|
|
465
|
-
try {
|
|
466
|
-
return await mintStudioLogin({ fetch: deps.fetch, origin: studio.url, adminKey });
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
deps.err(`Studio did not answer through the proxy at ${studio.url} (${errorText(error)}); signing in at ${origin} instead.`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
417
|
+
const origin = studioOrigin(studioPort);
|
|
472
418
|
const deadline = Date.now() + (deps.loginTimeoutMs ?? 15_000);
|
|
473
419
|
let lastError = "";
|
|
474
420
|
for (;;) {
|
|
@@ -584,7 +530,7 @@ async function start(deps, args) {
|
|
|
584
530
|
if (started.studioStarted) {
|
|
585
531
|
deps.out(`Studio ${started.studioUrl}`);
|
|
586
532
|
if (opensBrowser(deps, flags)) {
|
|
587
|
-
const login = await tryStudioLogin(ctx,
|
|
533
|
+
const login = await tryStudioLogin(ctx, started.studioPort, started.adminKey);
|
|
588
534
|
if (login)
|
|
589
535
|
await openLogin(ctx, withNext(login, tenantStudioPath(tenant.id)));
|
|
590
536
|
}
|
|
@@ -631,7 +577,7 @@ async function stop(deps, args) {
|
|
|
631
577
|
deps.out(`Stopped Tenant ${ctx.name}; its data is kept.`);
|
|
632
578
|
return 0;
|
|
633
579
|
}
|
|
634
|
-
/** `stop --all`: every running Tenant on this machine
|
|
580
|
+
/** `stop --all`: every running Tenant on this machine; volumes are kept. */
|
|
635
581
|
async function stopAll(deps) {
|
|
636
582
|
await dockerPreflight(deps.docker);
|
|
637
583
|
const projects = (await composeProjects(deps.docker)) ?? new Map();
|
|
@@ -648,17 +594,11 @@ async function stopAll(deps) {
|
|
|
648
594
|
throw new CliError(`docker compose stop failed for Tenant ${tenant.name} (exit ${code}).`, 1);
|
|
649
595
|
stopped.push(tenant.name);
|
|
650
596
|
}
|
|
651
|
-
const proxy = projects.get(PROXY_PROJECT) === "running" && (await stopProxy(deps.docker));
|
|
652
597
|
deps.out(stopped.length
|
|
653
|
-
? `Stopped Tenants ${stopped.join(", ")}
|
|
654
|
-
:
|
|
598
|
+
? `Stopped Tenants ${stopped.join(", ")}; their data is kept.`
|
|
599
|
+
: "No Tenant was running.");
|
|
655
600
|
return 0;
|
|
656
601
|
}
|
|
657
|
-
async function studioUrls(ctx, port) {
|
|
658
|
-
const url = await studioUrl(ctx, port);
|
|
659
|
-
const direct = studioOrigin(port);
|
|
660
|
-
return url === direct ? { url } : { url: direct, proxyUrl: url };
|
|
661
|
-
}
|
|
662
602
|
async function stackStatus(ctx) {
|
|
663
603
|
const base = {
|
|
664
604
|
name: ctx.name,
|
|
@@ -703,7 +643,7 @@ async function stackStatus(ctx) {
|
|
|
703
643
|
},
|
|
704
644
|
...(tenant ? { tenant } : {}),
|
|
705
645
|
studio: {
|
|
706
|
-
...(persisted.studioPort ?
|
|
646
|
+
...(persisted.studioPort ? { url: studioOrigin(persisted.studioPort) } : {}),
|
|
707
647
|
state: studio ? [studio.state, studio.health].filter(Boolean).join(", ") : "absent",
|
|
708
648
|
...(persisted.studioFrameAncestors
|
|
709
649
|
? { embedOrigins: persisted.studioFrameAncestors }
|
|
@@ -719,13 +659,9 @@ async function stackStatus(ctx) {
|
|
|
719
659
|
services,
|
|
720
660
|
};
|
|
721
661
|
}
|
|
722
|
-
/** The Studio proxy, as data (`nylorun doctor`). */
|
|
723
|
-
export async function readProxyReport(deps) {
|
|
724
|
-
return await readProxyStatus(deps.docker, nylorunRoot(deps), deps.env);
|
|
725
|
-
}
|
|
726
662
|
/** What `nylorun status` reports, as data (`nylorun doctor`). */
|
|
727
663
|
export async function readStackStatus(deps, options = {}) {
|
|
728
|
-
await
|
|
664
|
+
await tidyMachine(deps);
|
|
729
665
|
return await stackStatus(await selectStack(deps, options));
|
|
730
666
|
}
|
|
731
667
|
function describeTenant(tenant) {
|
|
@@ -760,8 +696,7 @@ async function status(deps, args) {
|
|
|
760
696
|
out(`Runtime ${result.runtime.url ?? "?"} ${runtimeDetail}`);
|
|
761
697
|
if (result.runtime.adminUrl)
|
|
762
698
|
out(`Admin API ${result.runtime.adminUrl} (operators only, never proxied)`);
|
|
763
|
-
|
|
764
|
-
out(`Studio ${result.studio.proxyUrl ?? result.studio.url ?? "?"} ${result.studio.state} (${also}log in with "nylorun studio")`);
|
|
699
|
+
out(`Studio ${result.studio.url ?? "?"} ${result.studio.state} (log in with "nylorun studio")`);
|
|
765
700
|
if (result.studio.embedOrigins?.length)
|
|
766
701
|
out(`Embeds ${result.studio.embedOrigins.join(" ")} (may show Studio in a frame)`);
|
|
767
702
|
out(`Gateway ${result.gateway.state} (the Model Gate; model calls fail while it is down)`);
|
|
@@ -848,7 +783,6 @@ async function ls(deps, args) {
|
|
|
848
783
|
const memory = [...(projects?.values() ?? [])].includes("running")
|
|
849
784
|
? await tenantMemory(deps.docker)
|
|
850
785
|
: undefined;
|
|
851
|
-
const proxy = proxyDisabled(deps.env) ? undefined : await readProxySettings(base);
|
|
852
786
|
const tenants = [];
|
|
853
787
|
for (const entry of await listTenants(base)) {
|
|
854
788
|
const persisted = (await readStackEnv(stackPaths(entry.root))) ?? {};
|
|
@@ -862,13 +796,7 @@ async function ls(deps, args) {
|
|
|
862
796
|
...(persisted.runtimePort
|
|
863
797
|
? { runtimeUrl: `http://${STACK_CLIENT_HOST}:${persisted.runtimePort}` }
|
|
864
798
|
: {}),
|
|
865
|
-
...(persisted.studioPort
|
|
866
|
-
? {
|
|
867
|
-
studioUrl: proxy
|
|
868
|
-
? proxyOrigin(entry.name, proxy.port)
|
|
869
|
-
: studioOrigin(persisted.studioPort),
|
|
870
|
-
}
|
|
871
|
-
: {}),
|
|
799
|
+
...(persisted.studioPort ? { studioUrl: studioOrigin(persisted.studioPort) } : {}),
|
|
872
800
|
ports: {
|
|
873
801
|
...(persisted.runtimePort ? { runtime: persisted.runtimePort } : {}),
|
|
874
802
|
...(persisted.adminPort ? { admin: persisted.adminPort } : {}),
|
|
@@ -920,7 +848,6 @@ async function deleteStack(deps, args) {
|
|
|
920
848
|
? composeArgs(ctx, "down", "--volumes", "--remove-orphans")
|
|
921
849
|
: ["compose", "--project-name", ctx.project, "down", "--volumes", "--remove-orphans"]);
|
|
922
850
|
await rm(ctx.paths.root, { recursive: true, force: true });
|
|
923
|
-
await refreshProxyRoutes(deps.docker, base, await proxyRoutes(deps));
|
|
924
851
|
deps.out(`Deleted Tenant ${name}: its containers, volumes and Host root (vault key included).`);
|
|
925
852
|
return 0;
|
|
926
853
|
}
|
|
@@ -955,7 +882,7 @@ async function runningStack(ctx, options) {
|
|
|
955
882
|
hostId: host.hostId,
|
|
956
883
|
adminKey,
|
|
957
884
|
studioPort: persisted.studioPort,
|
|
958
|
-
studioUrl:
|
|
885
|
+
studioUrl: studioOrigin(persisted.studioPort),
|
|
959
886
|
studioUp,
|
|
960
887
|
started: false,
|
|
961
888
|
};
|
|
@@ -1005,7 +932,7 @@ export function tenantStudioPath(tenantId) {
|
|
|
1005
932
|
* Undefined (after a warning) when Studio does not answer.
|
|
1006
933
|
*/
|
|
1007
934
|
export async function studioLoginUrl(deps, running, next) {
|
|
1008
|
-
const login = await tryStudioLogin({ deps },
|
|
935
|
+
const login = await tryStudioLogin({ deps }, running.studioPort, running.adminKey);
|
|
1009
936
|
return login === undefined ? undefined : withNext(login, next);
|
|
1010
937
|
}
|
|
1011
938
|
async function studio(deps, args, options = {}) {
|
|
@@ -1035,7 +962,6 @@ async function studio(deps, args, options = {}) {
|
|
|
1035
962
|
deps.out(`Studio ${login}`);
|
|
1036
963
|
return 0;
|
|
1037
964
|
}
|
|
1038
|
-
// The login's origin: the proxy's, or Studio's own port when the proxy did not answer.
|
|
1039
965
|
const origin = new URL(login).origin;
|
|
1040
966
|
deps.out(`Studio ${next ? new URL(next, origin).toString() : origin}`);
|
|
1041
967
|
await openLogin({ deps }, login);
|
|
@@ -1043,7 +969,7 @@ async function studio(deps, args, options = {}) {
|
|
|
1043
969
|
}
|
|
1044
970
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
1045
971
|
export async function runStudioCommand(args, deps, options = {}) {
|
|
1046
|
-
await
|
|
972
|
+
await tidyMachine(deps);
|
|
1047
973
|
return await studio(deps, args, options);
|
|
1048
974
|
}
|
|
1049
975
|
const COMMANDS = {
|
|
@@ -1067,6 +993,6 @@ export async function runStackCommand(name, args, deps) {
|
|
|
1067
993
|
const command = COMMANDS[name];
|
|
1068
994
|
if (!command)
|
|
1069
995
|
throw usageError(`Unknown command ${name}.\n${stackUsage}`);
|
|
1070
|
-
await
|
|
996
|
+
await tidyMachine(deps);
|
|
1071
997
|
return await command(deps, args);
|
|
1072
998
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
|
|
7
7
|
* `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
|
|
8
8
|
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
|
|
9
|
-
* UID/GID, the Host root, the images
|
|
9
|
+
* UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
10
10
|
*
|
|
11
11
|
* The Tenant's state is its Postgres database, executed through Restate, with its
|
|
12
12
|
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
@@ -7,7 +7,7 @@ import { PINNED_IMAGES } from "./images.js";
|
|
|
7
7
|
* every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
|
|
8
8
|
* `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
|
|
9
9
|
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
|
|
10
|
-
* UID/GID, the Host root, the images
|
|
10
|
+
* UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
11
11
|
*
|
|
12
12
|
* The Tenant's state is its Postgres database, executed through Restate, with its
|
|
13
13
|
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
@@ -72,9 +72,6 @@ services:
|
|
|
72
72
|
command: ["--node-name=restate-1"] # stable name, so data is found on restart
|
|
73
73
|
environment:
|
|
74
74
|
RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
|
|
75
|
-
# RocksDB's memory (block cache and memtables) defaults to 2 GiB; a local Tenant needs a
|
|
76
|
-
# fraction of it, and several Tenants share a laptop.
|
|
77
|
-
RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE: 256MiB
|
|
78
75
|
volumes:
|
|
79
76
|
- restate:/restate-data
|
|
80
77
|
- \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
|
|
@@ -209,10 +206,8 @@ services:
|
|
|
209
206
|
NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
|
|
210
207
|
NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
|
|
211
208
|
PORT: "3000"
|
|
212
|
-
# Studio's Host check accepts localhost/127.0.0.1 on the published port
|
|
213
|
-
# proxy's origin (http://<name>.localhost:<port>), kept in .env.
|
|
209
|
+
# Studio's Host check accepts localhost/127.0.0.1 on the published port.
|
|
214
210
|
NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
|
|
215
|
-
NYLORUN_STUDIO_PUBLIC_ORIGINS: \${NYLORUN_STUDIO_PUBLIC_ORIGINS:-}
|
|
216
211
|
# Browsers share cookies across ports of one host: a cookie per Tenant keeps the
|
|
217
212
|
# sessions of two Studios on localhost apart.
|
|
218
213
|
NYLORUN_STUDIO_SESSION_COOKIE: nylorun_studio_${name}
|
package/dist/stack/env-file.d.ts
CHANGED
|
@@ -34,11 +34,6 @@ export interface StackEnv {
|
|
|
34
34
|
* when telemetry is off. Decided again on every start.
|
|
35
35
|
*/
|
|
36
36
|
studioAnalyticsId: string;
|
|
37
|
-
/**
|
|
38
|
-
* Origins Studio also serves besides localhost and 127.0.0.1 on its port, separated by
|
|
39
|
-
* spaces: the proxy's `http://<name>.localhost:<port>`, or empty. Decided on every start.
|
|
40
|
-
*/
|
|
41
|
-
studioPublicOrigins: string;
|
|
42
37
|
/** The Tenant's name, which the Runtime gives the Tenant it creates. */
|
|
43
38
|
tenantName: string;
|
|
44
39
|
/**
|
package/dist/stack/env-file.js
CHANGED
|
@@ -24,7 +24,6 @@ const KEYS = {
|
|
|
24
24
|
studioImage: "NYLORUN_STUDIO_IMAGE",
|
|
25
25
|
studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
|
|
26
26
|
studioAnalyticsId: "NYLORUN_STUDIO_ANALYTICS_ID",
|
|
27
|
-
studioPublicOrigins: "NYLORUN_STUDIO_PUBLIC_ORIGINS",
|
|
28
27
|
tenantName: "NYLORUN_TENANT_NAME",
|
|
29
28
|
derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
|
|
30
29
|
};
|
|
@@ -82,10 +81,6 @@ export function renderEnvFile(env) {
|
|
|
82
81
|
"# it off with nylorun telemetry disable or NYLORUN_TELEMETRY_DISABLED=1.",
|
|
83
82
|
line("studioAnalyticsId"),
|
|
84
83
|
"",
|
|
85
|
-
"# Studio's address through the proxy (http://<name>.localhost:<port>); empty",
|
|
86
|
-
"# without the proxy. Set on every start.",
|
|
87
|
-
line("studioPublicOrigins"),
|
|
88
|
-
"",
|
|
89
84
|
"# The Tenant's name, which the Runtime gives the Tenant it creates on the first",
|
|
90
85
|
"# start, and the derived principals it registers on that Tenant (keys derived",
|
|
91
86
|
"# from the admin key; `project` is the Project link's).",
|
package/dist/stack/images.d.ts
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Images a local Tenant runs. The Runtime and Studio tags are pinned by this
|
|
3
3
|
* CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
|
|
4
4
|
* (local builds, CI images). Postgres, Restate and s2-lite are the official
|
|
5
|
-
* images, pinned here
|
|
5
|
+
* images, pinned here.
|
|
6
6
|
*/
|
|
7
7
|
export declare const PINNED_IMAGES: {
|
|
8
8
|
readonly postgres: "postgres:17.11";
|
|
9
9
|
readonly restate: "docker.restate.dev/restatedev/restate:1.7.12";
|
|
10
10
|
readonly s2: "ghcr.io/s2-streamstore/s2:0.43.0";
|
|
11
|
-
readonly proxy: "caddy:2.11.6";
|
|
12
11
|
};
|
|
13
12
|
export interface StackImages {
|
|
14
13
|
runtime: string;
|
|
@@ -16,7 +15,6 @@ export interface StackImages {
|
|
|
16
15
|
postgres: string;
|
|
17
16
|
restate: string;
|
|
18
17
|
s2: string;
|
|
19
|
-
proxy: string;
|
|
20
18
|
}
|
|
21
19
|
/** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
|
|
22
20
|
export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;
|
package/dist/stack/images.js
CHANGED
|
@@ -3,13 +3,12 @@ import { pinnedVersion } from "./versions.js";
|
|
|
3
3
|
* Images a local Tenant runs. The Runtime and Studio tags are pinned by this
|
|
4
4
|
* CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
|
|
5
5
|
* (local builds, CI images). Postgres, Restate and s2-lite are the official
|
|
6
|
-
* images, pinned here
|
|
6
|
+
* images, pinned here.
|
|
7
7
|
*/
|
|
8
8
|
export const PINNED_IMAGES = {
|
|
9
9
|
postgres: "postgres:17.11",
|
|
10
10
|
restate: "docker.restate.dev/restatedev/restate:1.7.12",
|
|
11
11
|
s2: "ghcr.io/s2-streamstore/s2:0.43.0",
|
|
12
|
-
proxy: "caddy:2.11.6",
|
|
13
12
|
};
|
|
14
13
|
function override(env, name) {
|
|
15
14
|
const value = env[name]?.trim();
|
package/dist/stack/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { type StackDeps } from "./commands.js";
|
|
2
|
-
export { ensureStack, isStackCommand,
|
|
2
|
+
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
3
3
|
export type { StackDeps, StackEndpoints, StackStatus, StackTenant } from "./commands.js";
|
|
4
4
|
export { checkDocker } from "./docker.js";
|
|
5
5
|
export type { Check, DockerChecks } from "./docker.js";
|
|
6
|
-
export type { ProxyStatus } from "./proxy.js";
|
|
7
6
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
8
7
|
export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
|
|
9
8
|
/** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
|
package/dist/stack/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { runStackCommand, runStudioCommand } from "./commands.js";
|
|
|
4
4
|
import { spawnDocker } from "./docker.js";
|
|
5
5
|
import { pinnedVersion } from "./versions.js";
|
|
6
6
|
import { loopbackPorts } from "./ports.js";
|
|
7
|
-
export { ensureStack, isStackCommand,
|
|
7
|
+
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
8
8
|
export { checkDocker } from "./docker.js";
|
|
9
9
|
function browserCommand(env) {
|
|
10
10
|
if (process.platform === "darwin")
|
|
@@ -21,8 +21,6 @@ const STACK_ENV_OWNED = [
|
|
|
21
21
|
"NYLORUN_TENANT_NAME",
|
|
22
22
|
"NYLORUN_DERIVED_PRINCIPALS",
|
|
23
23
|
"NYLORUN_STUDIO_ANALYTICS_ID",
|
|
24
|
-
"NYLORUN_STUDIO_PUBLIC_ORIGINS",
|
|
25
|
-
"NYLORUN_PROXY_PORT",
|
|
26
24
|
];
|
|
27
25
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
28
26
|
export function defaultStackDeps(env) {
|
package/dist/stack/ports.d.ts
CHANGED
package/dist/stack/ports.js
CHANGED
package/dist/stack/prepare.d.ts
CHANGED
|
@@ -43,6 +43,4 @@ export declare function prepareStack(input: {
|
|
|
43
43
|
};
|
|
44
44
|
/** The measurement id Studio reports page views to; absent when telemetry is off. */
|
|
45
45
|
studioAnalyticsId?: string;
|
|
46
|
-
/** Studio's origin through the proxy; absent without the proxy. */
|
|
47
|
-
studioPublicOrigin?: string;
|
|
48
46
|
}): Promise<PreparedStack>;
|
package/dist/stack/prepare.js
CHANGED
|
@@ -77,7 +77,6 @@ export async function prepareStack(input) {
|
|
|
77
77
|
studioImage: input.images.studio,
|
|
78
78
|
studioFrameAncestors,
|
|
79
79
|
studioAnalyticsId: input.studioAnalyticsId ?? "",
|
|
80
|
-
studioPublicOrigins: input.studioPublicOrigin ?? "",
|
|
81
80
|
tenantName: input.name,
|
|
82
81
|
derivedPrincipals: (input.derivedPrincipals?.trim()
|
|
83
82
|
? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
|
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
|
+
}
|
|
@@ -3,8 +3,8 @@ export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>
|
|
|
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) on `origin` (its published port
|
|
7
|
-
*
|
|
6
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
7
|
+
* and return the URL to open there.
|
|
8
8
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
9
9
|
*/
|
|
10
10
|
export declare function mintStudioLogin(input: {
|
|
@@ -5,8 +5,8 @@ export function studioOrigin(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) on `origin` (its published port
|
|
9
|
-
*
|
|
8
|
+
* (`POST /_studio/login-tokens`, Studio §6) on `origin` (its published port)
|
|
9
|
+
* and return the URL to open there.
|
|
10
10
|
* Accepts `{ loginUrl }`, `{ url }` or `{ token }` in the response.
|
|
11
11
|
*/
|
|
12
12
|
export async function mintStudioLogin(input) {
|
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",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
},
|
|
43
43
|
"nylorun": {
|
|
44
44
|
"runtime": "0.15.2-beta",
|
|
45
|
-
"studio": "0.
|
|
45
|
+
"studio": "0.18.0-beta"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/dist/stack/proxy.d.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { DockerRunner } from "./docker.js";
|
|
2
|
-
import { type PortProbe } from "./ports.js";
|
|
3
|
-
/**
|
|
4
|
-
* The Studio proxy: one small Caddy container per machine that gives each local Tenant's
|
|
5
|
-
* Studio the address `http://<name>.localhost:<port>`. Browsers keep cookies per host, not
|
|
6
|
-
* per port, so separate hosts keep the Studios' sessions apart. It routes browsers to Studio
|
|
7
|
-
* only (programs keep the Runtime's `http://localhost:<port>`: Node's resolver need not
|
|
8
|
-
* resolve `*.localhost`) and holds no Tenant data: its files are `~/.nylorun/proxy/`
|
|
9
|
-
* (`compose.yaml`, `Caddyfile`, `.env`), and Caddy's own state is tmpfs.
|
|
10
|
-
*
|
|
11
|
-
* The proxy joins each Tenant's network (named after its Compose project) and reaches Studio
|
|
12
|
-
* by container name, `<project>-studio:3000`. Docker cannot remove a network the proxy is
|
|
13
|
-
* attached to, so `reset` and `delete` disconnect it first.
|
|
14
|
-
*/
|
|
15
|
-
export declare const PROXY_PROJECT = "nylorun-proxy";
|
|
16
|
-
export declare const PROXY_CONTAINER = "nylorun-proxy";
|
|
17
|
-
export interface ProxyPaths {
|
|
18
|
-
dir: string;
|
|
19
|
-
compose: string;
|
|
20
|
-
caddyfile: string;
|
|
21
|
-
/** The port, and whether Docker took the `::1` binding. */
|
|
22
|
-
env: string;
|
|
23
|
-
}
|
|
24
|
-
export declare function proxyPaths(base: string): ProxyPaths;
|
|
25
|
-
/** `NYLORUN_PROXY_DISABLED=1`: no proxy; Studio is `http://localhost:<port>`. */
|
|
26
|
-
export declare function proxyDisabled(env: Readonly<Record<string, string | undefined>>): boolean;
|
|
27
|
-
export declare function proxyOrigin(name: string, port: number): string;
|
|
28
|
-
export interface ProxySettings {
|
|
29
|
-
port: number;
|
|
30
|
-
/** Also published on `[::1]` (false once Docker refused it). */
|
|
31
|
-
ipv6: boolean;
|
|
32
|
-
}
|
|
33
|
-
export declare function readProxySettings(base: string): Promise<ProxySettings | undefined>;
|
|
34
|
-
/**
|
|
35
|
-
* The proxy's settings. On first use its port is 4160 when free, else a free one outside
|
|
36
|
-
* `taken`; it is kept after that.
|
|
37
|
-
*/
|
|
38
|
-
export declare function ensureProxySettings(base: string, ports: PortProbe, taken: ReadonlySet<number>): Promise<ProxySettings>;
|
|
39
|
-
/** A Tenant the proxy routes to: its name and Compose project (also its network's name). */
|
|
40
|
-
export interface ProxyRoute {
|
|
41
|
-
name: string;
|
|
42
|
-
project: string;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* One site per Tenant. Site addresses carry no port: Caddy listens on 80 in the container,
|
|
46
|
-
* and its host match ignores the published port in the browser's Host. The browser's Host
|
|
47
|
-
* (`<name>.localhost:<port>`) reaches Studio unchanged, which Studio checks; a Tenant that is
|
|
48
|
-
* not running answers 502, and an unknown host 404.
|
|
49
|
-
*/
|
|
50
|
-
export declare function renderCaddyfile(routes: readonly ProxyRoute[]): string;
|
|
51
|
-
export declare function renderProxyCompose(ipv6: boolean): string;
|
|
52
|
-
/** Detach the proxy from a Tenant's network, so Compose can remove it; never fails. */
|
|
53
|
-
export declare function disconnectProxy(docker: DockerRunner, network: string): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Write the routes, bring the proxy up (Compose recreates it when its file changed), attach
|
|
56
|
-
* it to every routed Tenant's network and reload Caddy. Never throws: a problem is warned
|
|
57
|
-
* once and the caller shows Studio's own port. When Docker refuses `[::1]` (IPv6 off), the
|
|
58
|
-
* proxy listens on 127.0.0.1 only, and says so once.
|
|
59
|
-
*/
|
|
60
|
-
export declare function startProxy(input: {
|
|
61
|
-
docker: DockerRunner;
|
|
62
|
-
base: string;
|
|
63
|
-
settings: ProxySettings;
|
|
64
|
-
routes: readonly ProxyRoute[];
|
|
65
|
-
err(line: string): void;
|
|
66
|
-
pollMs?: number;
|
|
67
|
-
}): Promise<boolean>;
|
|
68
|
-
/** Rewrite the routes of an existing proxy and reload it (`delete`); never fails. */
|
|
69
|
-
export declare function refreshProxyRoutes(docker: DockerRunner, base: string, routes: readonly ProxyRoute[]): Promise<void>;
|
|
70
|
-
/** Stop the proxy (`stop --all`); true when it stopped. */
|
|
71
|
-
export declare function stopProxy(docker: DockerRunner): Promise<boolean>;
|
|
72
|
-
export interface ProxyStatus {
|
|
73
|
-
state: "running" | "stopped" | "absent" | "disabled";
|
|
74
|
-
port?: number;
|
|
75
|
-
ipv6?: boolean;
|
|
76
|
-
}
|
|
77
|
-
/** The proxy as `nylorun doctor` reports it. */
|
|
78
|
-
export declare function readProxyStatus(docker: DockerRunner, base: string, env: Readonly<Record<string, string | undefined>>): Promise<ProxyStatus>;
|
package/dist/stack/proxy.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { parseEnvLines } from "./env-file.js";
|
|
6
|
-
import { PINNED_IMAGES } from "./images.js";
|
|
7
|
-
import { choosePort, DEFAULT_PORTS } from "./ports.js";
|
|
8
|
-
/**
|
|
9
|
-
* The Studio proxy: one small Caddy container per machine that gives each local Tenant's
|
|
10
|
-
* Studio the address `http://<name>.localhost:<port>`. Browsers keep cookies per host, not
|
|
11
|
-
* per port, so separate hosts keep the Studios' sessions apart. It routes browsers to Studio
|
|
12
|
-
* only (programs keep the Runtime's `http://localhost:<port>`: Node's resolver need not
|
|
13
|
-
* resolve `*.localhost`) and holds no Tenant data: its files are `~/.nylorun/proxy/`
|
|
14
|
-
* (`compose.yaml`, `Caddyfile`, `.env`), and Caddy's own state is tmpfs.
|
|
15
|
-
*
|
|
16
|
-
* The proxy joins each Tenant's network (named after its Compose project) and reaches Studio
|
|
17
|
-
* by container name, `<project>-studio:3000`. Docker cannot remove a network the proxy is
|
|
18
|
-
* attached to, so `reset` and `delete` disconnect it first.
|
|
19
|
-
*/
|
|
20
|
-
export const PROXY_PROJECT = "nylorun-proxy";
|
|
21
|
-
export const PROXY_CONTAINER = "nylorun-proxy";
|
|
22
|
-
export function proxyPaths(base) {
|
|
23
|
-
const dir = join(base, "proxy");
|
|
24
|
-
return {
|
|
25
|
-
dir,
|
|
26
|
-
compose: join(dir, "compose.yaml"),
|
|
27
|
-
caddyfile: join(dir, "Caddyfile"),
|
|
28
|
-
env: join(dir, ".env"),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/** `NYLORUN_PROXY_DISABLED=1`: no proxy; Studio is `http://localhost:<port>`. */
|
|
32
|
-
export function proxyDisabled(env) {
|
|
33
|
-
return /^(1|true)$/i.test(env.NYLORUN_PROXY_DISABLED?.trim() ?? "");
|
|
34
|
-
}
|
|
35
|
-
export function proxyOrigin(name, port) {
|
|
36
|
-
return `http://${name}.localhost:${port}`;
|
|
37
|
-
}
|
|
38
|
-
export async function readProxySettings(base) {
|
|
39
|
-
let text;
|
|
40
|
-
try {
|
|
41
|
-
text = await readFile(proxyPaths(base).env, "utf8");
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
const values = parseEnvLines(text);
|
|
47
|
-
const port = Number(values.get("NYLORUN_PROXY_PORT"));
|
|
48
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
49
|
-
return undefined;
|
|
50
|
-
return { port, ipv6: values.get("NYLORUN_PROXY_IPV6") !== "false" };
|
|
51
|
-
}
|
|
52
|
-
async function writeAtomic(path, text) {
|
|
53
|
-
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
54
|
-
await writeFile(temporary, text, { mode: 0o644 });
|
|
55
|
-
await rename(temporary, path);
|
|
56
|
-
}
|
|
57
|
-
async function writeProxySettings(base, settings) {
|
|
58
|
-
const paths = proxyPaths(base);
|
|
59
|
-
await mkdir(paths.dir, { recursive: true, mode: 0o755 });
|
|
60
|
-
await writeAtomic(paths.env, [
|
|
61
|
-
"# Written by `nylorun start`: the Studio proxy's port, kept from its first start,",
|
|
62
|
-
"# and whether it is also published on [::1].",
|
|
63
|
-
`NYLORUN_PROXY_PORT=${settings.port}`,
|
|
64
|
-
`NYLORUN_PROXY_IPV6=${settings.ipv6}`,
|
|
65
|
-
"",
|
|
66
|
-
].join("\n"));
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The proxy's settings. On first use its port is 4160 when free, else a free one outside
|
|
70
|
-
* `taken`; it is kept after that.
|
|
71
|
-
*/
|
|
72
|
-
export async function ensureProxySettings(base, ports, taken) {
|
|
73
|
-
const existing = await readProxySettings(base);
|
|
74
|
-
if (existing)
|
|
75
|
-
return existing;
|
|
76
|
-
const settings = { port: await choosePort(ports, DEFAULT_PORTS.proxy, undefined, taken), ipv6: true };
|
|
77
|
-
await writeProxySettings(base, settings);
|
|
78
|
-
return settings;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* One site per Tenant. Site addresses carry no port: Caddy listens on 80 in the container,
|
|
82
|
-
* and its host match ignores the published port in the browser's Host. The browser's Host
|
|
83
|
-
* (`<name>.localhost:<port>`) reaches Studio unchanged, which Studio checks; a Tenant that is
|
|
84
|
-
* not running answers 502, and an unknown host 404.
|
|
85
|
-
*/
|
|
86
|
-
export function renderCaddyfile(routes) {
|
|
87
|
-
const sites = routes.map(({ name, project }) => `http://${name}.localhost {
|
|
88
|
-
reverse_proxy ${project}-studio:3000
|
|
89
|
-
handle_errors {
|
|
90
|
-
respond \`Tenant ${name} is not running. Run "nylorun start --tenant ${name}".\` 502
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
`);
|
|
94
|
-
return [
|
|
95
|
-
`# Written by \`nylorun start\`: a site per Tenant under ~/.nylorun/tenants. Rewritten on every start.
|
|
96
|
-
{
|
|
97
|
-
auto_https off
|
|
98
|
-
admin localhost:2019 # inside the container only; \`caddy reload\` uses it
|
|
99
|
-
}
|
|
100
|
-
`,
|
|
101
|
-
...sites,
|
|
102
|
-
`:80 {
|
|
103
|
-
respond \`No Tenant at {host}. "nylorun ls" lists the Tenants on this machine.\` 404
|
|
104
|
-
}
|
|
105
|
-
`,
|
|
106
|
-
].join("\n");
|
|
107
|
-
}
|
|
108
|
-
export function renderProxyCompose(ipv6) {
|
|
109
|
-
return `# Written by \`nylorun start\`; rewritten on every start. The port lives in .env.
|
|
110
|
-
name: ${PROXY_PROJECT}
|
|
111
|
-
|
|
112
|
-
services:
|
|
113
|
-
proxy: # http://<tenant>.localhost:<port>: every local Tenant's Studio
|
|
114
|
-
image: ${PINNED_IMAGES.proxy}
|
|
115
|
-
container_name: ${PROXY_CONTAINER}
|
|
116
|
-
labels:
|
|
117
|
-
dev.nylorun.proxy: "true"
|
|
118
|
-
volumes:
|
|
119
|
-
- .:/etc/caddy:ro # this directory: the Caddyfile, reloaded with \`caddy reload\`
|
|
120
|
-
tmpfs: [/config, /data] # Caddy's own state; nothing to keep
|
|
121
|
-
ports:
|
|
122
|
-
- "127.0.0.1:\${NYLORUN_PROXY_PORT:?run nylorun start}:80"${ipv6
|
|
123
|
-
? `
|
|
124
|
-
- "[::1]:\${NYLORUN_PROXY_PORT:?run nylorun start}:80" # macOS resolves <name>.localhost to ::1 only`
|
|
125
|
-
: ""}
|
|
126
|
-
restart: unless-stopped
|
|
127
|
-
|
|
128
|
-
networks:
|
|
129
|
-
default:
|
|
130
|
-
name: ${PROXY_PROJECT}
|
|
131
|
-
labels:
|
|
132
|
-
dev.nylorun.proxy: "true"
|
|
133
|
-
`;
|
|
134
|
-
}
|
|
135
|
-
function proxyCompose(paths, ...args) {
|
|
136
|
-
return ["compose", "--project-name", PROXY_PROJECT, "--file", paths.compose, "--env-file", paths.env, ...args];
|
|
137
|
-
}
|
|
138
|
-
function lastLine(text) {
|
|
139
|
-
return text.trim().split(/\r?\n/).at(-1) ?? "";
|
|
140
|
-
}
|
|
141
|
-
/** Attach the proxy to a Tenant's network; "already attached" and "no such network" are fine. */
|
|
142
|
-
async function connectProxy(docker, network) {
|
|
143
|
-
const result = await docker.run(["network", "connect", network, PROXY_CONTAINER]);
|
|
144
|
-
if (result.code === 0 || /already exists|not found|no such network/i.test(result.stderr))
|
|
145
|
-
return undefined;
|
|
146
|
-
return lastLine(result.stderr) || `exit ${result.code}`;
|
|
147
|
-
}
|
|
148
|
-
/** Detach the proxy from a Tenant's network, so Compose can remove it; never fails. */
|
|
149
|
-
export async function disconnectProxy(docker, network) {
|
|
150
|
-
await docker.run(["network", "disconnect", "--force", network, PROXY_CONTAINER]);
|
|
151
|
-
}
|
|
152
|
-
async function reloadProxy(docker) {
|
|
153
|
-
return await docker.run(["exec", PROXY_CONTAINER, "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Write the routes, bring the proxy up (Compose recreates it when its file changed), attach
|
|
157
|
-
* it to every routed Tenant's network and reload Caddy. Never throws: a problem is warned
|
|
158
|
-
* once and the caller shows Studio's own port. When Docker refuses `[::1]` (IPv6 off), the
|
|
159
|
-
* proxy listens on 127.0.0.1 only, and says so once.
|
|
160
|
-
*/
|
|
161
|
-
export async function startProxy(input) {
|
|
162
|
-
const { docker, base, err } = input;
|
|
163
|
-
const paths = proxyPaths(base);
|
|
164
|
-
const warn = (problem) => err(`Warning: the Studio proxy is not available (${problem}); Studio is on its own port. See "nylorun doctor".`);
|
|
165
|
-
try {
|
|
166
|
-
await writeAtomic(paths.caddyfile, renderCaddyfile(input.routes));
|
|
167
|
-
await writeAtomic(paths.compose, renderProxyCompose(input.settings.ipv6));
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
warn(error instanceof Error ? error.message : String(error));
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
let up = await docker.run(proxyCompose(paths, "up", "--detach"));
|
|
174
|
-
if (up.code !== 0 && input.settings.ipv6) {
|
|
175
|
-
await writeAtomic(paths.compose, renderProxyCompose(false));
|
|
176
|
-
const ipv4 = await docker.run(proxyCompose(paths, "up", "--detach"));
|
|
177
|
-
if (ipv4.code === 0) {
|
|
178
|
-
await writeProxySettings(base, { ...input.settings, ipv6: false });
|
|
179
|
-
err(`Docker refused the Studio proxy on [::1]:${input.settings.port} (${lastLine(up.stderr)}); it listens on 127.0.0.1 only, so a browser that resolves *.localhost to ::1 alone does not reach it.`);
|
|
180
|
-
}
|
|
181
|
-
up = ipv4;
|
|
182
|
-
}
|
|
183
|
-
if (up.code !== 0) {
|
|
184
|
-
warn(`docker compose up: ${lastLine(up.stderr) || `exit ${up.code}`}`);
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
for (const route of input.routes) {
|
|
188
|
-
const problem = await connectProxy(docker, route.project);
|
|
189
|
-
if (problem)
|
|
190
|
-
err(`Warning: the Studio proxy could not join ${route.project} (${problem}).`);
|
|
191
|
-
}
|
|
192
|
-
// A container Compose just created may not take the reload yet.
|
|
193
|
-
for (let attempt = 1;; attempt += 1) {
|
|
194
|
-
const reload = await reloadProxy(docker);
|
|
195
|
-
if (reload.code === 0)
|
|
196
|
-
return true;
|
|
197
|
-
if (attempt === 10) {
|
|
198
|
-
warn(`caddy reload: ${lastLine(reload.stderr) || `exit ${reload.code}`}`);
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
await new Promise((resolve) => setTimeout(resolve, input.pollMs ?? 500));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
/** Rewrite the routes of an existing proxy and reload it (`delete`); never fails. */
|
|
205
|
-
export async function refreshProxyRoutes(docker, base, routes) {
|
|
206
|
-
const paths = proxyPaths(base);
|
|
207
|
-
if (!existsSync(paths.caddyfile))
|
|
208
|
-
return;
|
|
209
|
-
await writeAtomic(paths.caddyfile, renderCaddyfile(routes));
|
|
210
|
-
await reloadProxy(docker);
|
|
211
|
-
}
|
|
212
|
-
/** Stop the proxy (`stop --all`); true when it stopped. */
|
|
213
|
-
export async function stopProxy(docker) {
|
|
214
|
-
return (await docker.run(["compose", "--project-name", PROXY_PROJECT, "stop"])).code === 0;
|
|
215
|
-
}
|
|
216
|
-
/** The proxy as `nylorun doctor` reports it. */
|
|
217
|
-
export async function readProxyStatus(docker, base, env) {
|
|
218
|
-
if (proxyDisabled(env))
|
|
219
|
-
return { state: "disabled" };
|
|
220
|
-
const settings = await readProxySettings(base);
|
|
221
|
-
const inspect = await docker.run(["inspect", "--format", "{{.State.Status}}", PROXY_CONTAINER]);
|
|
222
|
-
const state = inspect.code !== 0 ? "absent" : inspect.stdout.trim() === "running" ? "running" : "stopped";
|
|
223
|
-
return { state, ...(settings ? { port: settings.port, ipv6: settings.ipv6 } : {}) };
|
|
224
|
-
}
|