@monster-orange/den 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1655 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +619 -3
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +9 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/agent-actor.ts +38 -3
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +63 -0
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +185 -7
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/environment-manager/catalog.ts +52 -1
- package/src/shared/environment-manager/default-workspace.ts +26 -3
- package/src/shared/environment-manager/git-remote.ts +49 -0
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/environment-manager/workspace-manager.ts +67 -25
- package/src/shared/environment-manager/workspace-org.ts +26 -11
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +328 -0
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +24 -5
- package/src/ui/cli/clone-plan.ts +154 -23
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +62 -3
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +649 -72
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/org-command.ts +12 -2
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/up-args.ts +15 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +298 -0
- package/src/ui/mcp/server.ts +70 -8
- package/src/ui/mcp/tools.ts +151 -24
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +90 -3
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +29 -45
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +590 -45
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +15 -3
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +24 -4
- package/src/ui/server/release-service.ts +25 -3
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +55 -9
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -12,9 +12,17 @@
|
|
|
12
12
|
* publish: true # false: another proxy fronts the edge
|
|
13
13
|
* http_port: 80
|
|
14
14
|
* https_port: 443
|
|
15
|
+
* tls: none # http only; or (see tls.ts):
|
|
16
|
+
* # tls: { strategy: mkcert } # own local CA, per machine
|
|
17
|
+
* # tls: { strategy: mkcert, ca: ~/path/to/ca } # an existing CA; never minted
|
|
18
|
+
* # tls: { strategy: files, include: edge/dynamic, mounts: {...} }
|
|
19
|
+
* # # included config holds the certs; optional ca: the CA that
|
|
20
|
+
* # # signed them, trusted by health checks
|
|
21
|
+
* # legacy (still read, mapped to tls, deprecated):
|
|
22
|
+
* # caroot: <ca> → tls: { strategy: mkcert, ca: <ca> }
|
|
23
|
+
* # issue_cert: false → tls: { strategy: files }
|
|
24
|
+
* # neither, no tls → mkcert with a local CA, and a warning
|
|
15
25
|
* # opt-ins (all off by default):
|
|
16
|
-
* caroot: ~/path/to/mkcert-ca # use an existing CA; never mint one
|
|
17
|
-
* issue_cert: false # included config holds the certs
|
|
18
26
|
* https_redirect: false # included config redirects per name
|
|
19
27
|
* docker_labels: { network: apps-network }
|
|
20
28
|
* include: edge/dynamic # dynamic-config files, repo-relative
|
|
@@ -35,7 +43,8 @@ import { cachedRead } from "../read-cache.js";
|
|
|
35
43
|
import { readManifestStacks } from "../stacks/config.js";
|
|
36
44
|
import { getActiveWorkspaces } from "../environment-manager/workspaces.js";
|
|
37
45
|
import { assertDomain } from "./render.js";
|
|
38
|
-
import
|
|
46
|
+
import { tlsStrategyIds } from "./tls.js";
|
|
47
|
+
import type { EdgeConfig, EdgeTls } from "./types.js";
|
|
39
48
|
|
|
40
49
|
import { product } from "../product.js";
|
|
41
50
|
export const DEFAULT_TRAEFIK_IMAGE = "traefik:v3.7.17";
|
|
@@ -51,6 +60,7 @@ interface RawEdge {
|
|
|
51
60
|
publish?: boolean;
|
|
52
61
|
http_port?: number;
|
|
53
62
|
https_port?: number;
|
|
63
|
+
tls?: unknown;
|
|
54
64
|
caroot?: string;
|
|
55
65
|
issue_cert?: boolean;
|
|
56
66
|
https_redirect?: boolean;
|
|
@@ -120,6 +130,108 @@ function mounts(v: RawEdge["mounts"], root: string): Record<string, string> {
|
|
|
120
130
|
return out;
|
|
121
131
|
}
|
|
122
132
|
|
|
133
|
+
const TLS_KEYS: Record<string, string[]> = {
|
|
134
|
+
none: ["strategy"],
|
|
135
|
+
mkcert: ["strategy", "ca"],
|
|
136
|
+
files: ["strategy", "ca", "include", "mounts"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
interface ResolvedTls {
|
|
140
|
+
tls: EdgeTls;
|
|
141
|
+
from: EdgeConfig["tlsFrom"];
|
|
142
|
+
/** `files`' own include and mounts (raw, resolved by the caller). */
|
|
143
|
+
include?: string;
|
|
144
|
+
mounts?: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `edge.tls` (user.yml's replaces the team's), or the legacy keys mapped to
|
|
149
|
+
* it (EXT-1 rule 6): `issue_cert: false` → files, `caroot` → mkcert with
|
|
150
|
+
* `ca`, neither → mkcert with a local CA (implicit, warned).
|
|
151
|
+
*/
|
|
152
|
+
function readTls(team: RawEdge, user: RawEdge, root: string): ResolvedTls {
|
|
153
|
+
const raw = user.tls ?? team.tls;
|
|
154
|
+
const caroot = user.caroot ?? team.caroot;
|
|
155
|
+
const issue = user.issue_cert ?? team.issue_cert;
|
|
156
|
+
const ids = tlsStrategyIds();
|
|
157
|
+
const builtIns = ids.join(", ");
|
|
158
|
+
if (raw === undefined) {
|
|
159
|
+
const ca = caroot ? resolvePath(caroot, root) : null;
|
|
160
|
+
return {
|
|
161
|
+
tls:
|
|
162
|
+
issue === false
|
|
163
|
+
? { strategy: "files", ca }
|
|
164
|
+
: { strategy: "mkcert", ca },
|
|
165
|
+
from: caroot === undefined && issue === undefined ? "implicit" : "legacy",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const legacy = [
|
|
169
|
+
...(caroot !== undefined ? ["caroot"] : []),
|
|
170
|
+
...(issue !== undefined ? ["issue_cert"] : []),
|
|
171
|
+
];
|
|
172
|
+
if (legacy.length)
|
|
173
|
+
throw new Error(
|
|
174
|
+
`edge.tls replaces edge.${legacy.join(" and edge.")}; remove ${legacy.length > 1 ? "them" : "it"} ` +
|
|
175
|
+
"(caroot is tls.ca, issue_cert: false is tls.strategy: files).",
|
|
176
|
+
);
|
|
177
|
+
const obj: Record<string, unknown> =
|
|
178
|
+
typeof raw === "string"
|
|
179
|
+
? { strategy: raw }
|
|
180
|
+
: raw && typeof raw === "object" && !Array.isArray(raw)
|
|
181
|
+
? (raw as Record<string, unknown>)
|
|
182
|
+
: {};
|
|
183
|
+
const id = obj.strategy;
|
|
184
|
+
if (typeof id !== "string" || !id)
|
|
185
|
+
throw new Error(
|
|
186
|
+
`edge.tls must be a strategy id or { strategy: <id>, ... }; the built-ins are ${builtIns}`,
|
|
187
|
+
);
|
|
188
|
+
if (!ids.includes(id))
|
|
189
|
+
throw new Error(
|
|
190
|
+
`edge.tls.strategy "${id}" is not a known TLS strategy; the built-ins are ${builtIns}`,
|
|
191
|
+
);
|
|
192
|
+
const allowed = TLS_KEYS[id] ?? ["strategy"];
|
|
193
|
+
for (const k of Object.keys(obj))
|
|
194
|
+
if (!allowed.includes(k))
|
|
195
|
+
throw new Error(
|
|
196
|
+
`edge.tls.${k} does not apply to strategy ${id} (it takes ${allowed.join(", ")})`,
|
|
197
|
+
);
|
|
198
|
+
let ca: string | null = null;
|
|
199
|
+
if (obj.ca !== undefined) {
|
|
200
|
+
if (typeof obj.ca !== "string" || !obj.ca.trim())
|
|
201
|
+
throw new Error(
|
|
202
|
+
"edge.tls.ca must be the path to an existing CA directory",
|
|
203
|
+
);
|
|
204
|
+
ca = resolvePath(obj.ca, root);
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
obj.include !== undefined &&
|
|
208
|
+
(typeof obj.include !== "string" || !obj.include)
|
|
209
|
+
)
|
|
210
|
+
throw new Error(
|
|
211
|
+
"edge.tls.include must be a directory of dynamic-config files",
|
|
212
|
+
);
|
|
213
|
+
if (
|
|
214
|
+
obj.mounts !== undefined &&
|
|
215
|
+
(!obj.mounts || typeof obj.mounts !== "object" || Array.isArray(obj.mounts))
|
|
216
|
+
)
|
|
217
|
+
throw new Error("edge.tls.mounts must map host paths to container paths");
|
|
218
|
+
const tls: EdgeTls =
|
|
219
|
+
id === "none"
|
|
220
|
+
? { strategy: "none" }
|
|
221
|
+
: { strategy: id as "mkcert" | "files", ca };
|
|
222
|
+
return {
|
|
223
|
+
tls,
|
|
224
|
+
from: "explicit",
|
|
225
|
+
include: obj.include as string | undefined,
|
|
226
|
+
mounts: obj.mounts as Record<string, string> | undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** True when the org config has an `edge:` block (an edge is configured). */
|
|
231
|
+
export function hasEdgeBlock(root: string = getDistRoot()): boolean {
|
|
232
|
+
return layers(root).team.edge !== undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
123
235
|
/** The web UI's route name: the brand name as a slug ("Den" → "den"). */
|
|
124
236
|
function webRouteName(team: Record<string, unknown>): string {
|
|
125
237
|
const name = (team.brand as { name?: string } | undefined)?.name?.trim();
|
|
@@ -188,6 +300,22 @@ function computeEdgeConfig(root: string): EdgeConfig {
|
|
|
188
300
|
throw new Error("edge.aliases must be a list of domains");
|
|
189
301
|
aliases.forEach(assertDomain);
|
|
190
302
|
const publish = user.publish ?? team.publish ?? true;
|
|
303
|
+
const tls = readTls(team, user, root);
|
|
304
|
+
if (tls.include !== undefined && team.include !== undefined)
|
|
305
|
+
throw new Error(
|
|
306
|
+
"edge.include and edge.tls.include are both set; keep one (tls.include for files' certs)",
|
|
307
|
+
);
|
|
308
|
+
const include = tls.include ?? team.include;
|
|
309
|
+
// The state dir the config names (or the XDG default), DEN_EDGE_DIR aside.
|
|
310
|
+
const configuredStateDir = expandHome(
|
|
311
|
+
user.state_dir ??
|
|
312
|
+
team.state_dir ??
|
|
313
|
+
path.join(
|
|
314
|
+
process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"),
|
|
315
|
+
team.org,
|
|
316
|
+
"edge",
|
|
317
|
+
),
|
|
318
|
+
);
|
|
191
319
|
return {
|
|
192
320
|
org: team.org,
|
|
193
321
|
domain: team.domain,
|
|
@@ -214,26 +342,24 @@ function computeEdgeConfig(root: string): EdgeConfig {
|
|
|
214
342
|
),
|
|
215
343
|
}
|
|
216
344
|
: null,
|
|
217
|
-
stateDir:
|
|
218
|
-
process.env.DEN_EDGE_DIR
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
caroot:
|
|
229
|
-
(user.caroot ?? team.caroot)
|
|
230
|
-
? resolvePath((user.caroot ?? team.caroot)!, root)
|
|
231
|
-
: null,
|
|
232
|
-
issueCert: (user.issue_cert ?? team.issue_cert) !== false,
|
|
345
|
+
stateDir:
|
|
346
|
+
process.env.DEN_EDGE_DIR !== undefined
|
|
347
|
+
? expandHome(process.env.DEN_EDGE_DIR)
|
|
348
|
+
: configuredStateDir,
|
|
349
|
+
// Set only when DEN_EDGE_DIR overrides it: the dir the config names.
|
|
350
|
+
...(process.env.DEN_EDGE_DIR !== undefined &&
|
|
351
|
+
expandHome(process.env.DEN_EDGE_DIR) !== configuredStateDir
|
|
352
|
+
? { defaultStateDir: configuredStateDir }
|
|
353
|
+
: {}),
|
|
354
|
+
tls: tls.tls,
|
|
355
|
+
tlsFrom: tls.from,
|
|
233
356
|
httpsRedirect: (user.https_redirect ?? team.https_redirect) !== false,
|
|
234
357
|
dockerLabels: dockerLabels(user.docker_labels ?? team.docker_labels),
|
|
235
|
-
include:
|
|
236
|
-
mounts: mounts(
|
|
358
|
+
include: include ? resolvePath(include, root) : null,
|
|
359
|
+
mounts: mounts(
|
|
360
|
+
{ ...(team.mounts ?? {}), ...(user.mounts ?? {}), ...(tls.mounts ?? {}) },
|
|
361
|
+
root,
|
|
362
|
+
),
|
|
237
363
|
networks: [
|
|
238
364
|
...new Set([
|
|
239
365
|
...list(team.networks, "networks"),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Running the edge's CLIs (docker, mkcert): one place, one error shape. */
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
export function sh(
|
|
5
|
+
cmd: string,
|
|
6
|
+
args: string[],
|
|
7
|
+
opts: { env?: NodeJS.ProcessEnv; inherit?: boolean } = {},
|
|
8
|
+
) {
|
|
9
|
+
const r = spawnSync(cmd, args, {
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
env: { ...process.env, ...opts.env },
|
|
12
|
+
stdio: opts.inherit ? "inherit" : "pipe",
|
|
13
|
+
});
|
|
14
|
+
if (r.error) throw new Error(`${cmd} is not installed (${r.error.message})`);
|
|
15
|
+
return {
|
|
16
|
+
code: r.status ?? 1,
|
|
17
|
+
out: (r.stdout ?? "").trim(),
|
|
18
|
+
err: (r.stderr ?? "").trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function must(
|
|
23
|
+
cmd: string,
|
|
24
|
+
args: string[],
|
|
25
|
+
opts: { env?: NodeJS.ProcessEnv } = {},
|
|
26
|
+
): string {
|
|
27
|
+
const r = sh(cmd, args, opts);
|
|
28
|
+
if (r.code !== 0)
|
|
29
|
+
throw new Error(`${cmd} ${args.join(" ")} failed:\n${r.err || r.out}`);
|
|
30
|
+
return r.out;
|
|
31
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routers in the distribution's included dynamic-config files (`edge
|
|
3
|
+
* include`, e.g. `edge/dynamic/*.yml`): Traefik serves them beside the
|
|
4
|
+
* routes Den generates, so `edge routes`, /api/edge and the Edge page list
|
|
5
|
+
* them too, each marked by its source file (PL-SPEC §10, a consumer's review).
|
|
6
|
+
*
|
|
7
|
+
* Read-only: the files are parsed, never written, and only `http.routers`
|
|
8
|
+
* is read. The same files `edge up` copies in (top-level YAML, not the
|
|
9
|
+
* generated edge.yml, not a stack's own route file).
|
|
10
|
+
*
|
|
11
|
+
* Since 0.3.0-dev.32 (a consumer's finding) also the stacks' own route
|
|
12
|
+
* files: Den writes `stack-<project>.yml` into the edge's dynamic directory
|
|
13
|
+
* when a stack comes up, and those routers are neither Den's host routes
|
|
14
|
+
* nor included files, so nothing listed them.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { parse } from "yaml";
|
|
19
|
+
import { includeFiles, isOwnedElsewhere, isStackRoutes } from "./runtime.js";
|
|
20
|
+
import type { EdgeConfig } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export interface IncludedRouter {
|
|
23
|
+
/** The router's key under `http.routers`. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** The included file it comes from (its name, as Traefik reads it). */
|
|
26
|
+
file: string;
|
|
27
|
+
rule: string;
|
|
28
|
+
service: string;
|
|
29
|
+
entryPoints: string[];
|
|
30
|
+
/** The hosts its rule names (`Host(...)`), for links. */
|
|
31
|
+
hosts: string[];
|
|
32
|
+
tls: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Served over https: the router has TLS or is on the websecure
|
|
35
|
+
* entrypoint (0.3.0-dev.33). Its URLs are https://, else http://.
|
|
36
|
+
*/
|
|
37
|
+
secure: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Where it sends traffic: its service's loadBalancer server URL(s) when
|
|
40
|
+
* the same file defines that service, else the service's name
|
|
41
|
+
* (0.3.0-dev.33).
|
|
42
|
+
*/
|
|
43
|
+
upstream: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A router's URLs: one per host, with its scheme; none without a Host(). */
|
|
47
|
+
export function routerUrls(r: Pick<IncludedRouter, "hosts" | "secure">) {
|
|
48
|
+
const scheme = r.secure ? "https://" : "http://";
|
|
49
|
+
return r.hosts.map((h) => scheme + h);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IncludedRouters {
|
|
53
|
+
routers: IncludedRouter[];
|
|
54
|
+
/** Files that could not be read or parsed: listed, never fatal. */
|
|
55
|
+
errors: { file: string; error: string }[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The hosts a Traefik rule names: every quoted argument of Host(...). */
|
|
59
|
+
export function ruleHosts(rule: string): string[] {
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
for (const m of rule.matchAll(/\bHost\(([^)]*)\)/g))
|
|
62
|
+
for (const q of m[1].matchAll(/[`"']([^`"']+)[`"']/g))
|
|
63
|
+
if (!out.includes(q[1])) out.push(q[1]);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
68
|
+
|
|
69
|
+
/** The routers in one dynamic-config document's `http.routers`. */
|
|
70
|
+
export function routersOf(doc: unknown, file: string): IncludedRouter[] {
|
|
71
|
+
const http = (
|
|
72
|
+
doc as { http?: { routers?: unknown; services?: unknown } } | null
|
|
73
|
+
)?.http;
|
|
74
|
+
const routers = http?.routers;
|
|
75
|
+
if (!routers || typeof routers !== "object" || Array.isArray(routers))
|
|
76
|
+
return [];
|
|
77
|
+
const services = obj(http?.services);
|
|
78
|
+
return Object.entries(routers as Record<string, unknown>).map(([name, r]) => {
|
|
79
|
+
const o = obj(r);
|
|
80
|
+
const rule = str(o.rule);
|
|
81
|
+
const service = str(o.service);
|
|
82
|
+
const entryPoints = Array.isArray(o.entryPoints)
|
|
83
|
+
? o.entryPoints.filter((e): e is string => typeof e === "string")
|
|
84
|
+
: [];
|
|
85
|
+
const tls = o.tls !== undefined && o.tls !== null && o.tls !== false;
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
file,
|
|
89
|
+
rule,
|
|
90
|
+
service,
|
|
91
|
+
entryPoints,
|
|
92
|
+
hosts: ruleHosts(rule),
|
|
93
|
+
tls,
|
|
94
|
+
secure: tls || entryPoints.includes("websecure"),
|
|
95
|
+
upstream: serverUrls(services[service]).join(", ") || service,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const obj = (v: unknown): Record<string, unknown> =>
|
|
101
|
+
v && typeof v === "object" && !Array.isArray(v)
|
|
102
|
+
? (v as Record<string, unknown>)
|
|
103
|
+
: {};
|
|
104
|
+
|
|
105
|
+
/** A Traefik service's loadBalancer server URLs (none: not a load balancer). */
|
|
106
|
+
function serverUrls(svc: unknown): string[] {
|
|
107
|
+
const servers = obj(obj(svc).loadBalancer).servers;
|
|
108
|
+
return Array.isArray(servers)
|
|
109
|
+
? servers.map((x) => str(obj(x).url)).filter(Boolean)
|
|
110
|
+
: [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Every router in the edge's included files, in file then key order. */
|
|
114
|
+
export function includedRouters(cfg: EdgeConfig): IncludedRouters {
|
|
115
|
+
const out: IncludedRouters = { routers: [], errors: [] };
|
|
116
|
+
if (!cfg.include) return out;
|
|
117
|
+
let files: string[];
|
|
118
|
+
try {
|
|
119
|
+
files = includeFiles(cfg.include).filter((f) => !isOwnedElsewhere(f));
|
|
120
|
+
} catch (e) {
|
|
121
|
+
out.errors.push({
|
|
122
|
+
file: path.basename(cfg.include),
|
|
123
|
+
error: (e as Error).message,
|
|
124
|
+
});
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
try {
|
|
129
|
+
const doc: unknown = parse(
|
|
130
|
+
fs.readFileSync(path.join(cfg.include, file), "utf-8"),
|
|
131
|
+
);
|
|
132
|
+
out.routers.push(...routersOf(doc, file));
|
|
133
|
+
} catch (e) {
|
|
134
|
+
out.errors.push({ file, error: (e as Error).message.split("\n")[0] });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Every router in the stacks' route files (`stack-*.yml` in the edge's
|
|
142
|
+
* dynamic directory, which `stack up` writes and `stack down` removes), in
|
|
143
|
+
* file then key order. No directory yet: none.
|
|
144
|
+
*/
|
|
145
|
+
export function stackRouters(cfg: EdgeConfig): IncludedRouters {
|
|
146
|
+
const out: IncludedRouters = { routers: [], errors: [] };
|
|
147
|
+
const dyn = path.join(cfg.stateDir, "dynamic");
|
|
148
|
+
let files: string[];
|
|
149
|
+
try {
|
|
150
|
+
files = fs
|
|
151
|
+
.readdirSync(dyn, { withFileTypes: true })
|
|
152
|
+
.filter((e) => e.isFile() && isStackRoutes(e.name))
|
|
153
|
+
.map((e) => e.name)
|
|
154
|
+
.sort();
|
|
155
|
+
} catch {
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
try {
|
|
160
|
+
const doc: unknown = parse(
|
|
161
|
+
fs.readFileSync(path.join(dyn, file), "utf-8"),
|
|
162
|
+
);
|
|
163
|
+
out.routers.push(...routersOf(doc, file));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
out.errors.push({ file, error: (e as Error).message.split("\n")[0] });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
@@ -38,12 +38,34 @@ function hostRule(name: string, cfg: EdgeConfig): string {
|
|
|
38
38
|
.join(" || ");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
41
|
+
/** True when the edge serves https: every edge.tls strategy but `none`. */
|
|
42
|
+
export function edgeHttps(cfg: EdgeConfig): boolean {
|
|
43
|
+
return cfg.tls.strategy !== "none";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The entrypoint routes listen on: websecure, or web for `tls: none`. */
|
|
47
|
+
export function routeEntryPoint(cfg: EdgeConfig): "web" | "websecure" {
|
|
48
|
+
return edgeHttps(cfg) ? "websecure" : "web";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A router's TLS block: `{ tls: {} }`, or nothing for `tls: none`. */
|
|
52
|
+
export function routerTls(cfg: EdgeConfig): { tls?: Record<string, never> } {
|
|
53
|
+
return edgeHttps(cfg) ? { tls: {} } : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Public URL for a route, including the port when it isn't the default
|
|
58
|
+
* (443, or 80 for an http-only edge).
|
|
59
|
+
*/
|
|
42
60
|
export function routeUrl(
|
|
43
61
|
name: string,
|
|
44
62
|
cfg: EdgeConfig,
|
|
45
63
|
domain = cfg.domain,
|
|
46
64
|
): string {
|
|
65
|
+
if (!edgeHttps(cfg)) {
|
|
66
|
+
const h = cfg.ports?.http ?? 80;
|
|
67
|
+
return `http://${routeHost(name, domain)}${h === 80 ? "" : `:${h}`}`;
|
|
68
|
+
}
|
|
47
69
|
const p = cfg.ports?.https ?? 443;
|
|
48
70
|
return `https://${routeHost(name, domain)}${p === 443 ? "" : `:${p}`}`;
|
|
49
71
|
}
|
|
@@ -64,12 +86,20 @@ export function certNames(cfg: EdgeConfig): string[] {
|
|
|
64
86
|
]);
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Dynamic config: TLS cert, HTTP→HTTPS redirect, dashboard, routes. With
|
|
91
|
+
* `tls: none` every router is on the http entrypoint, with no TLS and no
|
|
92
|
+
* redirect.
|
|
93
|
+
*/
|
|
68
94
|
export function renderEdgeDynamic(cfg: EdgeConfig): string {
|
|
69
95
|
const httpsPort = cfg.ports?.https ?? 443;
|
|
70
96
|
const redirectPort = httpsPort !== 443 ? { port: String(httpsPort) } : {};
|
|
97
|
+
const https = edgeHttps(cfg);
|
|
98
|
+
const redirect = https && cfg.httpsRedirect;
|
|
99
|
+
// Fresh objects per router: a shared one would render as a YAML anchor.
|
|
100
|
+
const entryPoints = () => [routeEntryPoint(cfg)];
|
|
71
101
|
const routers: Record<string, unknown> = {
|
|
72
|
-
...(
|
|
102
|
+
...(redirect
|
|
73
103
|
? {
|
|
74
104
|
"to-https": {
|
|
75
105
|
entryPoints: ["web"],
|
|
@@ -81,10 +111,10 @@ export function renderEdgeDynamic(cfg: EdgeConfig): string {
|
|
|
81
111
|
}
|
|
82
112
|
: {}),
|
|
83
113
|
dashboard: {
|
|
84
|
-
entryPoints:
|
|
114
|
+
entryPoints: entryPoints(),
|
|
85
115
|
rule: hostRule("edge", cfg),
|
|
86
116
|
service: "api@internal",
|
|
87
|
-
|
|
117
|
+
...routerTls(cfg),
|
|
88
118
|
},
|
|
89
119
|
};
|
|
90
120
|
const services: Record<string, unknown> = {};
|
|
@@ -97,10 +127,10 @@ export function renderEdgeDynamic(cfg: EdgeConfig): string {
|
|
|
97
127
|
);
|
|
98
128
|
const key = `route-${slug(name)}`;
|
|
99
129
|
routers[key] = {
|
|
100
|
-
entryPoints:
|
|
130
|
+
entryPoints: entryPoints(),
|
|
101
131
|
rule: hostRule(name, cfg),
|
|
102
132
|
service: key,
|
|
103
|
-
|
|
133
|
+
...routerTls(cfg),
|
|
104
134
|
};
|
|
105
135
|
services[key] = { loadBalancer: { servers: [{ url }] } };
|
|
106
136
|
}
|
|
@@ -113,7 +143,7 @@ export function renderEdgeDynamic(cfg: EdgeConfig): string {
|
|
|
113
143
|
yaml.stringify({
|
|
114
144
|
http: {
|
|
115
145
|
routers,
|
|
116
|
-
...(
|
|
146
|
+
...(redirect
|
|
117
147
|
? {
|
|
118
148
|
middlewares: {
|
|
119
149
|
"to-https": {
|
|
@@ -128,7 +158,7 @@ export function renderEdgeDynamic(cfg: EdgeConfig): string {
|
|
|
128
158
|
: {}),
|
|
129
159
|
...(Object.keys(services).length ? { services } : {}),
|
|
130
160
|
},
|
|
131
|
-
...(cfg.
|
|
161
|
+
...(cfg.tls.strategy === "mkcert"
|
|
132
162
|
? {
|
|
133
163
|
tls: {
|
|
134
164
|
stores: { default: { defaultCertificate: cert } },
|
|
@@ -145,8 +175,25 @@ export function dockerProxyName(org: string): string {
|
|
|
145
175
|
return `${edgeNames(org).container}-docker`;
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Compose file for the edge; publishes host ports only when configured.
|
|
180
|
+
* With `tls: none` there is no https entrypoint, port or cert volume.
|
|
181
|
+
*/
|
|
182
|
+
/** The labels that say which state dir and org own the edge container. */
|
|
183
|
+
export const EDGE_ROOT_LABEL = "den.edge.root";
|
|
184
|
+
export const EDGE_ORG_LABEL = "den.edge.org";
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The edge's compose.yml. Its container carries `den.edge.root` (the state
|
|
188
|
+
* dir that owns it, `ownerRoot`: edgeUp passes its realpath) and
|
|
189
|
+
* `den.edge.org` (0.3.0-dev.40): edgeUp and `edge down` touch an existing
|
|
190
|
+
* edge only when its root is theirs.
|
|
191
|
+
*/
|
|
192
|
+
export function renderEdgeCompose(
|
|
193
|
+
cfg: EdgeConfig,
|
|
194
|
+
ownerRoot: string = cfg.stateDir,
|
|
195
|
+
): string {
|
|
196
|
+
const https = edgeHttps(cfg);
|
|
150
197
|
const n = edgeNames(cfg.org);
|
|
151
198
|
const proxy = dockerProxyName(cfg.org);
|
|
152
199
|
const labels = cfg.dockerLabels;
|
|
@@ -159,10 +206,11 @@ export function renderEdgeCompose(cfg: EdgeConfig): string {
|
|
|
159
206
|
image: cfg.image,
|
|
160
207
|
container_name: n.container,
|
|
161
208
|
restart: "unless-stopped",
|
|
209
|
+
labels: { [EDGE_ROOT_LABEL]: ownerRoot, [EDGE_ORG_LABEL]: cfg.org },
|
|
162
210
|
...(labels ? { depends_on: ["docker-proxy"] } : {}),
|
|
163
211
|
command: [
|
|
164
212
|
"--entrypoints.web.address=:80",
|
|
165
|
-
"--entrypoints.websecure.address=:443",
|
|
213
|
+
...(https ? ["--entrypoints.websecure.address=:443"] : []),
|
|
166
214
|
"--providers.file.directory=/etc/edge/dynamic",
|
|
167
215
|
"--providers.file.watch=true",
|
|
168
216
|
...(labels
|
|
@@ -179,12 +227,19 @@ export function renderEdgeCompose(cfg: EdgeConfig): string {
|
|
|
179
227
|
...cfg.args,
|
|
180
228
|
],
|
|
181
229
|
...(cfg.ports
|
|
182
|
-
? {
|
|
230
|
+
? {
|
|
231
|
+
ports: [
|
|
232
|
+
`${cfg.ports.http}:80`,
|
|
233
|
+
...(https ? [`${cfg.ports.https}:443`] : []),
|
|
234
|
+
],
|
|
235
|
+
}
|
|
183
236
|
: {}),
|
|
184
237
|
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
185
238
|
volumes: [
|
|
186
239
|
"./dynamic:/etc/edge/dynamic:ro",
|
|
187
|
-
...(cfg.
|
|
240
|
+
...(cfg.tls.strategy === "mkcert"
|
|
241
|
+
? ["./certs:/etc/edge/certs:ro"]
|
|
242
|
+
: []),
|
|
188
243
|
...Object.entries(cfg.mounts)
|
|
189
244
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
190
245
|
.map(([host, target]) => `${host}:${target}:ro`),
|