@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
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/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REALM_APP_DIR_GID,
|
|
3
|
+
isPreparedAppDir,
|
|
4
|
+
scopeBindSource,
|
|
5
|
+
type WorkerApp,
|
|
6
|
+
type WorkerState,
|
|
7
|
+
} from "../../../worker-protocol/index.ts";
|
|
8
|
+
import { HARDENING } from "./default-hardening.ts";
|
|
9
|
+
import { buildContainerLabels } from "./labels.ts";
|
|
10
|
+
import { EFFECTS, type EffectContext } from "./effects.ts";
|
|
11
|
+
import type {
|
|
12
|
+
ComposeConfig,
|
|
13
|
+
ComposeDeploy,
|
|
14
|
+
ComposeHealthcheck,
|
|
15
|
+
ComposeNetworkDef,
|
|
16
|
+
ComposeService,
|
|
17
|
+
ComposeVolume,
|
|
18
|
+
} from "./types.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One app's rendered compose project. The worker runs each of these as
|
|
22
|
+
* its own `docker compose --project-name realm_<appId>` up, in the order
|
|
23
|
+
* returned by `convertToDockerCompose` (providers before consumers).
|
|
24
|
+
*/
|
|
25
|
+
export interface AppComposeProject {
|
|
26
|
+
appId: string;
|
|
27
|
+
compose: ComposeConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConvertOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Worker-host timezone (e.g. `"Europe/Stockholm"`), typically read
|
|
33
|
+
* from `/etc/timezone` by the worker spine. Stamped as `TZ=` on
|
|
34
|
+
* every service that doesn't already define its own `TZ` env. Apps
|
|
35
|
+
* win: setting `env.TZ` in realm.ts opts out for that service.
|
|
36
|
+
*/
|
|
37
|
+
tz?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Platform string (`linux/amd64`, `linux/arm64`) stamped on every
|
|
40
|
+
* service. Suppresses Docker's "platform mismatch" warning on hosts
|
|
41
|
+
* that emulate (e.g. Apple Silicon running amd64 images via Rosetta).
|
|
42
|
+
* Empty/unset omits the field, leaving Docker to pick.
|
|
43
|
+
*/
|
|
44
|
+
platform?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render a WorkerState — runtime-neutral, post-resolution
|
|
49
|
+
* data — into one docker-compose project per app. The worker runs each
|
|
50
|
+
* project under its own `--project-name realm_<appId>`, sequentially, in
|
|
51
|
+
* the returned order: providers before the apps that consume them.
|
|
52
|
+
*
|
|
53
|
+
* The renderer trusts the wire: refs are already walked, image digests
|
|
54
|
+
* are already pinned, cross-app `dependsOn` edges are pre-stitched. No
|
|
55
|
+
* grant checks, no peer-registry walks. Future runtimes
|
|
56
|
+
* (`convertToQuadlet`, `convertToKube`) operate on the same wire.
|
|
57
|
+
*
|
|
58
|
+
* Cross-app `depends_on` is NOT emitted into compose — it can't span
|
|
59
|
+
* projects. The topological order returned here carries that ordering
|
|
60
|
+
* instead; intra-app readiness still rides `restart: on-failure`.
|
|
61
|
+
*/
|
|
62
|
+
export function convertToDockerCompose(
|
|
63
|
+
state: WorkerState,
|
|
64
|
+
opts: ConvertOptions = {},
|
|
65
|
+
): AppComposeProject[] {
|
|
66
|
+
return topoSortApps(state.apps).map((app) => {
|
|
67
|
+
const services: Record<string, ComposeService> = {};
|
|
68
|
+
const networks: Record<string, ComposeNetworkDef> = {};
|
|
69
|
+
renderApp(app, services, networks, opts);
|
|
70
|
+
return {
|
|
71
|
+
appId: app.appId,
|
|
72
|
+
compose: {
|
|
73
|
+
services,
|
|
74
|
+
...(Object.keys(networks).length > 0 ? { networks } : {}),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Order apps so every provider comes before the apps that consume one of
|
|
82
|
+
* its services (Kahn's algorithm over the cross-app `dependsOn` provider
|
|
83
|
+
* edges). The worker runs the per-app ups in this order, so a consumer's
|
|
84
|
+
* provider is already up by the time its own up runs.
|
|
85
|
+
*
|
|
86
|
+
* Edges to apps not present in this wire are ignored. Apps left in a
|
|
87
|
+
* cycle (or with no edges) keep their input order and are appended last;
|
|
88
|
+
* they converge across reconciles via `restart: on-failure`.
|
|
89
|
+
*/
|
|
90
|
+
function topoSortApps(apps: WorkerApp[]): WorkerApp[] {
|
|
91
|
+
const present = new Set(apps.map((a) => a.appId));
|
|
92
|
+
// For each app, the set of in-wire provider appIds it depends on.
|
|
93
|
+
const providersOf = new Map<string, Set<string>>();
|
|
94
|
+
for (const app of apps) {
|
|
95
|
+
const providers = new Set<string>();
|
|
96
|
+
for (const edges of Object.values(app.dependsOn)) {
|
|
97
|
+
for (const edge of edges) {
|
|
98
|
+
if (edge.providerAppId !== app.appId && present.has(edge.providerAppId)) {
|
|
99
|
+
providers.add(edge.providerAppId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
providersOf.set(app.appId, providers);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sorted: WorkerApp[] = [];
|
|
107
|
+
const placed = new Set<string>();
|
|
108
|
+
// Each pass emits, in input order, every not-yet-placed app whose
|
|
109
|
+
// providers are all placed. A pass that places nothing means the rest
|
|
110
|
+
// is a cycle.
|
|
111
|
+
let progressed = true;
|
|
112
|
+
while (progressed && placed.size < apps.length) {
|
|
113
|
+
progressed = false;
|
|
114
|
+
for (const app of apps) {
|
|
115
|
+
if (placed.has(app.appId)) continue;
|
|
116
|
+
const ready = [...providersOf.get(app.appId)!].every((p) => placed.has(p));
|
|
117
|
+
if (!ready) continue;
|
|
118
|
+
sorted.push(app);
|
|
119
|
+
placed.add(app.appId);
|
|
120
|
+
progressed = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Cycle remainder: append in input order.
|
|
124
|
+
for (const app of apps) {
|
|
125
|
+
if (!placed.has(app.appId)) sorted.push(app);
|
|
126
|
+
}
|
|
127
|
+
return sorted;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderApp(
|
|
131
|
+
app: WorkerApp,
|
|
132
|
+
services: Record<string, ComposeService>,
|
|
133
|
+
networks: Record<string, ComposeNetworkDef>,
|
|
134
|
+
opts: ConvertOptions,
|
|
135
|
+
): void {
|
|
136
|
+
const { appId, services: appServices, egress, externalNetworks, allowedAbsoluteHostPaths } = app;
|
|
137
|
+
const exemptHostPaths = new Set(allowedAbsoluteHostPaths);
|
|
138
|
+
// Home pre-computes the app's union of external networks, and an EFFECT
|
|
139
|
+
// can add more per service during render. Both are honored.
|
|
140
|
+
const appExternalNetworks = new Set<string>(externalNetworks);
|
|
141
|
+
|
|
142
|
+
for (const [name, svc] of Object.entries(appServices)) {
|
|
143
|
+
const volumes: ComposeVolume[] = [];
|
|
144
|
+
// Whether this service mounts a directory the worker prepares with
|
|
145
|
+
// REALM_APP_DIR_GID + setgid. The container has to join that group or an
|
|
146
|
+
// image running as a baked-in uid can't write its own storage; the same
|
|
147
|
+
// predicate decides membership and what the worker prepares.
|
|
148
|
+
let ownsPreparedDirs = false;
|
|
149
|
+
if (svc.volumes) {
|
|
150
|
+
for (const v of svc.volumes) {
|
|
151
|
+
const hostPath = v.hostPath;
|
|
152
|
+
const source = exemptHostPaths.has(hostPath) ? hostPath : scopeBindSource(hostPath, appId);
|
|
153
|
+
if (isPreparedAppDir(appId, v, exemptHostPaths)) ownsPreparedDirs = true;
|
|
154
|
+
// Read-only is opt-in. A mount an app declared is a mount it means to
|
|
155
|
+
// use, and the overwhelmingly common case is writing to it.
|
|
156
|
+
volumes.push({
|
|
157
|
+
type: "bind",
|
|
158
|
+
source,
|
|
159
|
+
target: v.containerPath,
|
|
160
|
+
...(v.readOnly ? { read_only: true } : {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let environment: Record<string, string> | undefined;
|
|
166
|
+
if (svc.env) {
|
|
167
|
+
environment = {};
|
|
168
|
+
for (const [k, val] of Object.entries(svc.env)) {
|
|
169
|
+
environment[k] = String(val);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (opts.tz && !(environment && "TZ" in environment)) {
|
|
173
|
+
environment ??= {};
|
|
174
|
+
environment.TZ = opts.tz;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let healthcheck: ComposeHealthcheck | undefined;
|
|
178
|
+
if (svc.healthcheck) {
|
|
179
|
+
const hc = svc.healthcheck;
|
|
180
|
+
// Compose-spec rejects bare integers — durations need a unit suffix.
|
|
181
|
+
healthcheck = {
|
|
182
|
+
test: hc.test,
|
|
183
|
+
interval: `${hc.interval}s`,
|
|
184
|
+
timeout: `${hc.timeout}s`,
|
|
185
|
+
retries: hc.retries,
|
|
186
|
+
start_period: `${hc.startPeriod}s`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let deploy: ComposeDeploy | undefined;
|
|
191
|
+
const { memory, cpu, replicas } = svc;
|
|
192
|
+
if (memory != null || cpu != null || replicas > 1) {
|
|
193
|
+
deploy = {};
|
|
194
|
+
if (replicas > 1) deploy.replicas = replicas;
|
|
195
|
+
if (memory || cpu) {
|
|
196
|
+
deploy.resources = {
|
|
197
|
+
limits: {
|
|
198
|
+
...(memory ? { memory } : {}),
|
|
199
|
+
...(cpu ? { cpus: String(cpu) } : {}),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const labels = buildContainerLabels({
|
|
206
|
+
appId,
|
|
207
|
+
serviceName: name,
|
|
208
|
+
customLabels: svc.labels,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const composeService: ComposeService = {
|
|
212
|
+
image: svc.image,
|
|
213
|
+
...(opts.platform ? { platform: opts.platform } : {}),
|
|
214
|
+
...(environment ? { environment } : {}),
|
|
215
|
+
...(svc.command ? { command: svc.command } : {}),
|
|
216
|
+
labels,
|
|
217
|
+
...(volumes.length > 0 ? { volumes } : {}),
|
|
218
|
+
...(ownsPreparedDirs ? { group_add: [REALM_APP_DIR_GID] } : {}),
|
|
219
|
+
...(healthcheck ? { healthcheck } : {}),
|
|
220
|
+
...(deploy ? { deploy } : {}),
|
|
221
|
+
cap_drop: [...HARDENING.cap_drop],
|
|
222
|
+
cap_add: [...HARDENING.cap_add],
|
|
223
|
+
security_opt: [...HARDENING.security_opt],
|
|
224
|
+
pids_limit: HARDENING.pids_limit,
|
|
225
|
+
ulimits: { ...HARDENING.ulimits },
|
|
226
|
+
...(svc.shmSizeMb != null ? { shm_size: `${svc.shmSizeMb}m` } : {}),
|
|
227
|
+
restart: HARDENING.restart,
|
|
228
|
+
networks: { [`realm_${appId}`]: { aliases: [name] } },
|
|
229
|
+
logging: {
|
|
230
|
+
driver: HARDENING.logging.driver,
|
|
231
|
+
options: { ...HARDENING.logging.options },
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const filesUnused = new Map<string, string>();
|
|
236
|
+
const ctx: EffectContext = {
|
|
237
|
+
appId,
|
|
238
|
+
serviceName: name,
|
|
239
|
+
composeService,
|
|
240
|
+
externalNetworks: appExternalNetworks,
|
|
241
|
+
// The renderer doesn't materialize inline file bytes — Home ships them
|
|
242
|
+
// over the wire — but EFFECTS keeps the `files` parameter. No effect
|
|
243
|
+
// writes to it today; one that did would surface in the golden tests.
|
|
244
|
+
files: filesUnused,
|
|
245
|
+
};
|
|
246
|
+
for (const effect of Object.values(EFFECTS)) {
|
|
247
|
+
effect(svc, ctx);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// svc.files is keyed by container path; values are sha256 hashes Home
|
|
251
|
+
// pre-computed. Inline content is keyed by IDENTITY (sha256 of
|
|
252
|
+
// `appId:serviceKey:fileKey`), so the host path stays stable across content
|
|
253
|
+
// changes and a rewrite lands in place — what lets `--watch` consumers
|
|
254
|
+
// reload without a container recreate. Artifact refs carry their own hash.
|
|
255
|
+
if (svc.files != null) {
|
|
256
|
+
for (const [containerPath, sha] of Object.entries(svc.files)) {
|
|
257
|
+
composeService.volumes ??= [];
|
|
258
|
+
composeService.volumes.push({
|
|
259
|
+
type: "bind",
|
|
260
|
+
source: `./files/${sha}`,
|
|
261
|
+
target: containerPath,
|
|
262
|
+
read_only: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
services[`${appId}_${name}`] = composeService;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
networks[`realm_${appId}`] = {
|
|
271
|
+
name: `realm_${appId}`,
|
|
272
|
+
...(!egress ? { internal: true } : {}),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// External networks (management-net, peer `realm_<otherApp>`) get no
|
|
276
|
+
// top-level declaration: nothing in the `services` block references them.
|
|
277
|
+
// The worker connects to them post-up via `docker network connect`.
|
|
278
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Drop every capability, add back only what an ordinary image needs to boot:
|
|
2
|
+
// CHOWN to claim its own data dir, SETGID/SETUID for the gosu drop to a service
|
|
3
|
+
// user, NET_BIND_SERVICE to bind below 1024.
|
|
4
|
+
//
|
|
5
|
+
// DAC_OVERRIDE is here because without it those same entrypoints work only on
|
|
6
|
+
// their FIRST boot: uid 0 with no capabilities still faces ordinary permission
|
|
7
|
+
// checks, so once an image has tightened its own data dir (postgres chmods
|
|
8
|
+
// PGDATA to 0700 and refuses to start otherwise) root can no longer descend it
|
|
9
|
+
// and the entrypoint dies on the traversal. It costs little — the boundary is
|
|
10
|
+
// the mount gate in security.ts, not in-container DAC, and read-only mounts
|
|
11
|
+
// are unaffected (MS_RDONLY is VFS-enforced). These 5 are a subset of the 14
|
|
12
|
+
// capabilities Docker's own default grants.
|
|
13
|
+
export const HARDENING = {
|
|
14
|
+
cap_drop: ["ALL"],
|
|
15
|
+
cap_add: ["CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID", "NET_BIND_SERVICE"],
|
|
16
|
+
security_opt: ["no-new-privileges:true"],
|
|
17
|
+
pids_limit: 4096,
|
|
18
|
+
ulimits: { nofile: 65536, nproc: 4096 },
|
|
19
|
+
logging: { driver: "json-file", options: { "max-size": "10m", "max-file": "3" } },
|
|
20
|
+
restart: "on-failure",
|
|
21
|
+
} as const;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { HostPermissionKind } from "../../../permissions.ts";
|
|
2
|
+
import type { WireService } from "../../../worker-protocol/index.ts";
|
|
3
|
+
import type { ComposeService } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Compose effects table
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
//
|
|
9
|
+
// One entry per host permission kind. Each effect mutates the
|
|
10
|
+
// `ComposeService` (and the shared externalNetworks/files maps) from the
|
|
11
|
+
// resolved service's fields. The gate has already validated the service
|
|
12
|
+
// against grants by the time this runs, so effects only translate.
|
|
13
|
+
//
|
|
14
|
+
// `Record<HostPermissionKind, EffectFn>` keeps the table in lock-step with
|
|
15
|
+
// the kinds derived in permissions.ts: a new kind without an effect, or a
|
|
16
|
+
// stale effect for a deleted one, is a compile error.
|
|
17
|
+
|
|
18
|
+
export interface EffectContext {
|
|
19
|
+
appId: string;
|
|
20
|
+
serviceName: string;
|
|
21
|
+
composeService: ComposeService;
|
|
22
|
+
externalNetworks: Set<string>;
|
|
23
|
+
files: Map<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EffectFn = (svc: WireService, ctx: EffectContext) => void;
|
|
27
|
+
|
|
28
|
+
export const EFFECTS: Record<HostPermissionKind, EffectFn> = {
|
|
29
|
+
"host.gpu": (svc, ctx) => {
|
|
30
|
+
if (svc.gpu === true) {
|
|
31
|
+
ctx.composeService.devices = [...(ctx.composeService.devices ?? []), "/dev/dri:/dev/dri"];
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"host.port": (svc, ctx) => {
|
|
36
|
+
if (!svc.hostPorts || svc.hostPorts.length === 0) return;
|
|
37
|
+
const cs = ctx.composeService;
|
|
38
|
+
cs.ports ??= [];
|
|
39
|
+
for (const hp of svc.hostPorts) {
|
|
40
|
+
cs.ports.push({
|
|
41
|
+
target: hp.containerPort,
|
|
42
|
+
published: hp.hostPort,
|
|
43
|
+
...(hp.protocol ? { protocol: hp.protocol } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// A Silo's volume is mounted by an ordinary `volumes:` entry; the one thing
|
|
49
|
+
// the renderer adds is group membership, so it lands exactly where the mount
|
|
50
|
+
// lands, per service - a sidecar that doesn't mount the shared library never
|
|
51
|
+
// joins the group. `group_add` needs no cooperation from the image (unlike
|
|
52
|
+
// `user:`), so a root image, a baked-in uid, and a PUID all work.
|
|
53
|
+
"host.silo": (svc, ctx) => {
|
|
54
|
+
// Seeded from what's already there: a service that also mounts its own
|
|
55
|
+
// storage carries REALM_APP_DIR_GID from the base render, and joining a
|
|
56
|
+
// Silo must add to that membership rather than replace it - dropping it
|
|
57
|
+
// would take away the app's *own* directory to give it a shared one.
|
|
58
|
+
const gids = new Set<number>(ctx.composeService.group_add ?? []);
|
|
59
|
+
for (const v of svc.volumes ?? []) {
|
|
60
|
+
if (v.gid !== undefined) gids.add(v.gid);
|
|
61
|
+
}
|
|
62
|
+
if (gids.size > 0) ctx.composeService.group_add = [...gids].sort((a, b) => a - b);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// joinNetworks doesn't land in compose: the worker reconciles network
|
|
66
|
+
// membership post-up with `docker network connect/disconnect`, so adding
|
|
67
|
+
// or removing one doesn't recreate the container. The data rides the wire
|
|
68
|
+
// on `services[*].joinNetworks` and the worker reads it directly.
|
|
69
|
+
"host.management": () => {},
|
|
70
|
+
|
|
71
|
+
"host.egress": () => {},
|
|
72
|
+
|
|
73
|
+
// `iptables: true` rolls up the two raw primitives the firewall needs:
|
|
74
|
+
// NET_ADMIN to mutate netfilter, and host network mode so rules apply to
|
|
75
|
+
// host traffic and Docker's DOCKER-USER chain. Authoring stays at the
|
|
76
|
+
// intent level; the grant gate has already validated the use.
|
|
77
|
+
"host.iptables": (svc, ctx) => {
|
|
78
|
+
if (svc.iptables !== true) return;
|
|
79
|
+
if (!ctx.composeService.cap_add.includes("NET_ADMIN")) {
|
|
80
|
+
ctx.composeService.cap_add.push("NET_ADMIN");
|
|
81
|
+
}
|
|
82
|
+
ctx.composeService.network_mode = "host";
|
|
83
|
+
// host network mode and the per-project default network are
|
|
84
|
+
// mutually exclusive in compose.
|
|
85
|
+
delete ctx.composeService.networks;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// host.logs has no compose effect — the socket the value resolves to is
|
|
89
|
+
// mounted by the app via a normal `volumes:` entry. The grant exempts
|
|
90
|
+
// that mount from the absolute-mount gate (see graph.ts collectAllowed).
|
|
91
|
+
"host.logs": () => {},
|
|
92
|
+
|
|
93
|
+
// host.stats has no compose effect — same shape as host.logs: the app mounts
|
|
94
|
+
// the socket dir via a normal `volumes:` entry and the grant exempts it.
|
|
95
|
+
"host.stats": () => {},
|
|
96
|
+
|
|
97
|
+
// host.spawn has no compose effect — the socket/scratch/artifacts paths
|
|
98
|
+
// are mounted via normal `volumes:` entries and the network rides
|
|
99
|
+
// `joinNetworks`. The grant exempts all three from the absolute-mount /
|
|
100
|
+
// network.join gates (see graph.ts collectAllowed).
|
|
101
|
+
"host.spawn": () => {},
|
|
102
|
+
|
|
103
|
+
// No compose effect — `host.rocketLaunch` is a probe-the-UX permission
|
|
104
|
+
// that no worker advertises, so the gate blocks any app declaring it
|
|
105
|
+
// before it could ever reach this table.
|
|
106
|
+
"host.rocketLaunch": () => {},
|
|
107
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @yourrealm/sdk/runtime/docker/compose — docker-compose renderer.
|
|
2
|
+
//
|
|
3
|
+
// Transforms `WorkerState` (from @yourrealm/sdk/worker-protocol) into a
|
|
4
|
+
// docker-compose.yml. No I/O, no docker socket, no network. Home uses it in
|
|
5
|
+
// integration tests; the Worker uses it at reconcile time to produce the
|
|
6
|
+
// file `docker compose up` consumes.
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
type AppComposeProject,
|
|
10
|
+
convertToDockerCompose,
|
|
11
|
+
type ConvertOptions,
|
|
12
|
+
} from "./convert-to-docker-compose.ts";
|
|
13
|
+
export { HARDENING } from "./default-hardening.ts";
|
|
14
|
+
export { buildContainerLabels } from "./labels.ts";
|
|
15
|
+
export { EFFECTS, type EffectContext } from "./effects.ts";
|
|
16
|
+
export { composeSchema } from "./compose-schema.ts";
|
|
17
|
+
export type { ComposeConfig, ComposeNetworkDef, ComposeService } from "./types.ts";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { KINDS, LABELS } from "@local/docker";
|
|
2
|
+
|
|
3
|
+
interface ContainerLabelOptions {
|
|
4
|
+
appId: string;
|
|
5
|
+
serviceName: string;
|
|
6
|
+
customLabels?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const RESERVED_PREFIXES = ["realm.", "com.docker.", "org.opencontainers."];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Realm container metadata labels (managed, app, service). Routing
|
|
13
|
+
* data lives in the Caddy JSON config the proxy app generates from
|
|
14
|
+
* `peers.routedApps()`, not on container labels.
|
|
15
|
+
*
|
|
16
|
+
* Custom labels declared in realm.ts pass through unchanged unless they
|
|
17
|
+
* collide with a reserved prefix, in which case they're dropped.
|
|
18
|
+
*/
|
|
19
|
+
export function buildContainerLabels(options: ContainerLabelOptions): Record<string, string> {
|
|
20
|
+
const labels: Record<string, string> = {};
|
|
21
|
+
|
|
22
|
+
if (options.customLabels) {
|
|
23
|
+
for (const label of options.customLabels) {
|
|
24
|
+
const eqIdx = label.indexOf("=");
|
|
25
|
+
if (eqIdx === -1) continue;
|
|
26
|
+
const key = label.slice(0, eqIdx);
|
|
27
|
+
const keyLower = key.toLowerCase();
|
|
28
|
+
if (RESERVED_PREFIXES.some((p) => keyLower.startsWith(p))) continue;
|
|
29
|
+
labels[key] = label.slice(eqIdx + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
labels[LABELS.MANAGED] = "true";
|
|
34
|
+
labels[LABELS.KIND] = KINDS.APP_SERVICE;
|
|
35
|
+
labels[LABELS.APP] = options.appId;
|
|
36
|
+
labels[LABELS.SERVICE] = options.serviceName;
|
|
37
|
+
|
|
38
|
+
return labels;
|
|
39
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Compose-shape types the docker renderer emits. They live beside the
|
|
2
|
+
// renderer, which owns the docker dialect end-to-end; Home uses them only as
|
|
3
|
+
// TypeScript types via the @yourrealm/sdk/compile re-exports.
|
|
4
|
+
|
|
5
|
+
interface ComposeVolume {
|
|
6
|
+
type: "bind" | "volume";
|
|
7
|
+
source: string;
|
|
8
|
+
target: string;
|
|
9
|
+
read_only?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ComposePort {
|
|
13
|
+
target: number;
|
|
14
|
+
published: number;
|
|
15
|
+
protocol?: "tcp" | "udp";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ComposeHealthcheck {
|
|
19
|
+
test: string[];
|
|
20
|
+
interval?: string;
|
|
21
|
+
timeout?: string;
|
|
22
|
+
retries?: number;
|
|
23
|
+
start_period?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ComposeDeploy {
|
|
27
|
+
replicas?: number;
|
|
28
|
+
resources?: {
|
|
29
|
+
limits?: {
|
|
30
|
+
memory?: string;
|
|
31
|
+
cpus?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ComposeDependsOn {
|
|
37
|
+
[serviceName: string]: { condition: "service_started" | "service_healthy" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ComposeService {
|
|
41
|
+
image: string;
|
|
42
|
+
platform?: string;
|
|
43
|
+
environment?: Record<string, string>;
|
|
44
|
+
command?: string[];
|
|
45
|
+
entrypoint?: string[];
|
|
46
|
+
labels?: Record<string, string>;
|
|
47
|
+
volumes?: ComposeVolume[];
|
|
48
|
+
/** Supplementary groups, derived from the gid-carrying volumes mounted. */
|
|
49
|
+
group_add?: number[];
|
|
50
|
+
ports?: ComposePort[];
|
|
51
|
+
healthcheck?: ComposeHealthcheck;
|
|
52
|
+
deploy?: ComposeDeploy;
|
|
53
|
+
cap_drop: string[];
|
|
54
|
+
cap_add: string[];
|
|
55
|
+
security_opt: string[];
|
|
56
|
+
pids_limit: number;
|
|
57
|
+
ulimits: { nofile: number; nproc: number };
|
|
58
|
+
shm_size?: string;
|
|
59
|
+
devices?: string[];
|
|
60
|
+
network_mode?: string;
|
|
61
|
+
restart: string;
|
|
62
|
+
networks?: Record<string, { aliases?: string[] }>;
|
|
63
|
+
depends_on?: ComposeDependsOn;
|
|
64
|
+
logging: { driver: string; options: Record<string, string> };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ComposeNetworkDef {
|
|
68
|
+
external?: true;
|
|
69
|
+
internal?: true;
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ComposeConfig {
|
|
74
|
+
services: Record<string, ComposeService>;
|
|
75
|
+
networks?: Record<string, ComposeNetworkDef>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type { ComposeVolume, ComposePort, ComposeHealthcheck, ComposeDeploy, ComposeDependsOn };
|