okengine 0.2.8 → 0.3.2
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/AGENTS.md +17 -15
- package/README.md +57 -29
- package/package.json +11 -16
- package/site/content/docs/ai/llms-txt.mdx +54 -0
- package/site/content/docs/ai/mcp.mdx +123 -0
- package/site/content/docs/ai/meta.json +5 -0
- package/site/content/docs/ai/skills.mdx +53 -0
- package/site/content/docs/console/access.mdx +29 -0
- package/site/content/docs/console/ai.mdx +35 -0
- package/site/content/docs/console/architecture.mdx +35 -0
- package/site/content/docs/console/channels.mdx +37 -0
- package/site/content/docs/console/clock.mdx +31 -0
- package/site/content/docs/console/flows.mdx +31 -0
- package/site/content/docs/console/gates.mdx +35 -0
- package/site/content/docs/console/manifest-diff.mdx +34 -0
- package/site/content/docs/console/meta.json +23 -0
- package/site/content/docs/console/overview.mdx +40 -0
- package/site/content/docs/console/plugins.mdx +41 -0
- package/site/content/docs/console/privacy.mdx +32 -0
- package/site/content/docs/console/runs.mdx +40 -0
- package/site/content/docs/console/signals.mdx +31 -0
- package/site/content/docs/console/store.mdx +32 -0
- package/site/content/docs/console/tenancy.mdx +32 -0
- package/site/content/docs/console/traces.mdx +34 -0
- package/site/content/docs/console/vault.mdx +37 -0
- package/site/content/docs/elements/ai.mdx +180 -0
- package/site/content/docs/elements/channel.mdx +167 -0
- package/site/content/docs/elements/clock.mdx +182 -0
- package/site/content/docs/elements/flow.mdx +288 -0
- package/site/content/docs/elements/gate.mdx +171 -0
- package/site/content/docs/elements/meta.json +5 -0
- package/site/content/docs/elements/signal.mdx +171 -0
- package/site/content/docs/elements/store.mdx +320 -0
- package/site/content/docs/elements/vault.mdx +263 -0
- package/site/content/docs/get-started/basic-usage.mdx +124 -0
- package/site/content/docs/get-started/comparison.mdx +65 -0
- package/site/content/docs/get-started/installation.mdx +113 -0
- package/site/content/docs/get-started/introduction.mdx +123 -0
- package/site/content/docs/get-started/meta.json +5 -0
- package/site/content/docs/index.mdx +63 -0
- package/site/content/docs/meta.json +5 -0
- package/site/content/docs/plugins/compression.mdx +60 -0
- package/site/content/docs/plugins/cors.mdx +92 -0
- package/site/content/docs/plugins/csrf.mdx +96 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +92 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
- package/site/content/docs/plugins/meta.json +15 -0
- package/site/content/docs/plugins/security-headers.mdx +136 -0
- package/site/content/docs/reference/cli.md +101 -0
- package/site/content/docs/reference/configuration.mdx +159 -0
- package/site/content/docs/reference/environment-variables.mdx +87 -0
- package/site/content/docs/reference/errors.mdx +80 -0
- package/site/content/docs/reference/fx.mdx +117 -0
- package/site/content/docs/reference/meta.json +5 -0
- package/site/content/docs/reference/plugins.mdx +249 -0
- package/site/content/docs/reference/security.md +63 -0
- package/src/auth/auth.test.ts +3 -0
- package/src/cli/ask-dev-mode.ts +1 -1
- package/src/cli/db.ts +87 -17
- package/src/cli/dev-db-push.test.ts +32 -2
- package/src/cli/dev-schema-sync.test.ts +66 -0
- package/src/cli/dev-schema-sync.ts +139 -0
- package/src/cli/dev.test.ts +123 -1
- package/src/cli/dev.ts +150 -8
- package/src/cli/doc-staleness.test.ts +4 -4
- package/src/cli/docker-cli.test.ts +20 -0
- package/src/cli/docker.ts +10 -0
- package/src/cli/drizzle-env.test.ts +67 -0
- package/src/cli/drizzle-env.ts +78 -0
- package/src/cli/ensure-drizzle-config.ts +50 -0
- package/src/cli/hero-meta.test.ts +1 -1
- package/src/cli/load-config.ts +6 -0
- package/src/cli/mode.ts +24 -4
- package/src/cli/openbao-bootstrap.test.ts +147 -0
- package/src/cli/openbao-bootstrap.ts +280 -0
- package/src/cli/openbao-restart.integration.test.ts +136 -0
- package/src/cli/ports.test.ts +7 -5
- package/src/cli/ports.ts +6 -2
- package/src/cli/resolve-dev-sql-env.test.ts +48 -0
- package/src/cli/resolve-dev-sql-env.ts +42 -0
- package/src/cli/stack.ts +7 -4
- package/src/cli/vault-cmd.ts +63 -0
- package/src/client/types.ts +7 -1
- package/src/compiler/extract.test.ts +40 -0
- package/src/compiler/extract.ts +123 -1
- package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/response.ts +12 -0
- package/src/config/define-config.test.ts +4 -6
- package/src/config/index.ts +2 -15
- package/src/console/server/app.ts +2 -0
- package/src/console/server/vault.ts +21 -6
- package/src/docker/compose.ts +112 -16
- package/src/docker/derive.ts +7 -1
- package/src/docker/docker.test.ts +103 -0
- package/src/docker/index.ts +11 -1
- package/src/docker/recipes/index.ts +3 -2
- package/src/docker/recipes/openbao.ts +47 -0
- package/src/docker/recipes/redis.ts +5 -1
- package/src/docker/recipes/rustfs.ts +2 -3
- package/src/docker/stack-id.test.ts +43 -8
- package/src/docker/stack-id.ts +99 -20
- package/src/docker/stack.ts +36 -4
- package/src/docker/types.ts +3 -0
- package/src/docs-origin.ts +4 -4
- package/src/drivers/drizzle-dialect.test.ts +20 -0
- package/src/drivers/drizzle-dialect.ts +37 -0
- package/src/drivers/index.ts +1 -2
- package/src/drivers/memory.ts +278 -39
- package/src/drivers/s3.ts +10 -1
- package/src/drivers/vault-driver-removal.test.ts +55 -0
- package/src/drivers/vault-openbao.test.ts +97 -0
- package/src/drivers/vault-openbao.ts +102 -35
- package/src/drivers/vault-types.ts +3 -10
- package/src/elements/store/declare.ts +4 -1
- package/src/elements/store/resource-list-docs.fixture.ts +56 -0
- package/src/elements/store/resource-list-docs.test.ts +79 -0
- package/src/elements/store/resource.test.ts +253 -0
- package/src/elements/store/resource.ts +786 -0
- package/src/elements/store/sql-condition.test.ts +132 -0
- package/src/elements/store/sql-condition.ts +284 -46
- package/src/elements/store/sql-session.test.ts +86 -1
- package/src/elements/store/sql-session.ts +187 -27
- package/src/elements/store/table.ts +34 -4
- package/src/elements/store.ts +16 -0
- package/src/elements/vault/runtime.ts +1 -1
- package/src/elements/vault.test.ts +1 -28
- package/src/elements/vault.ts +1 -1
- package/src/kernel/app.ts +59 -25
- package/src/kernel/boot-bind/channel.test.ts +60 -0
- package/src/kernel/boot-bind/channel.ts +64 -2
- package/src/kernel/boot-bind/store.test.ts +10 -1
- package/src/kernel/boot-bind/store.ts +49 -2
- package/src/kernel/boot.test.ts +0 -1
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/edge.test.ts +68 -0
- package/src/kernel/errors.registry.test.ts +1 -1
- package/src/kernel/flow.ts +8 -0
- package/src/kernel/fx.test.ts +23 -3
- package/src/kernel/fx.ts +115 -18
- package/src/kernel/hooks.test.ts +33 -0
- package/src/kernel/hooks.ts +22 -0
- package/src/kernel/index.ts +7 -0
- package/src/kernel/on.ts +44 -3
- package/src/kernel/plugin.ts +33 -3
- package/src/kernel/registry-isolation.test.ts +74 -0
- package/src/kernel/registry.ts +22 -1
- package/src/kernel/triggers.ts +59 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/manifest/index.ts +1 -1
- package/src/manifest/types.ts +1 -1
- package/src/manifest/validate.ts +2 -2
- package/src/plugins/compression.test.ts +127 -0
- package/src/plugins/compression.ts +94 -0
- package/src/plugins/config-source.test.ts +204 -0
- package/src/plugins/config-source.ts +209 -0
- package/src/plugins/cors.test.ts +138 -0
- package/src/plugins/cors.ts +129 -0
- package/src/plugins/csrf.test.ts +102 -0
- package/src/plugins/csrf.ts +86 -0
- package/src/plugins/headers.ts +54 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/ip-allowlist.test.ts +105 -0
- package/src/plugins/ip-allowlist.ts +76 -0
- package/src/plugins/maintenance-mode.test.ts +91 -0
- package/src/plugins/maintenance-mode.ts +85 -0
- package/src/plugins/security-headers.test.ts +243 -0
- package/src/plugins/security-headers.ts +255 -0
- package/src/release/measure.ts +1 -2
- package/src/test/create-test-app.ts +14 -2
- package/docs/spec/console.md +0 -762
- package/docs/spec/example.md +0 -1374
- package/docs/spec/four-applications.md +0 -1376
- package/docs/spec/unified-theory.md +0 -498
- package/src/cli/doc-drift.test.ts +0 -147
- package/src/cli/doc-drift.ts +0 -401
- package/src/cli/doctor-diff-examples.ts +0 -90
- package/src/drivers/vault-sops.ts +0 -246
- /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
package/src/docker/compose.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
} from "./types.ts";
|
|
19
19
|
import { DEFAULT_DOCKER_DIR } from "./types.ts";
|
|
20
20
|
import { generateCredentials } from "./credentials.ts";
|
|
21
|
-
import { hostPortForInstance,
|
|
21
|
+
import { extraHostPortForInstance, hostPortForInstance, STACK_CONTROL_KEYS } from "./stack-id.ts";
|
|
22
22
|
import { APP_PORT } from "../runtime/types.ts";
|
|
23
23
|
|
|
24
24
|
/** Canonical layer-4 filename — never written by derivation. */
|
|
@@ -110,13 +110,17 @@ export function emitComposeLayers(
|
|
|
110
110
|
files.push({ path: "compose.yml", content: `${toYaml(base)}\n` });
|
|
111
111
|
|
|
112
112
|
// Layer 2 — per-role
|
|
113
|
-
const extraHostOffset = options.instanceId ? instancePortOffset(options.instanceId) : 0;
|
|
114
113
|
for (const spec of specs) {
|
|
115
114
|
const recipe = recipeFor(spec.image, recipes);
|
|
116
115
|
const applied = recipe.apply(spec);
|
|
117
116
|
const ports = [
|
|
118
117
|
`${spec.hostPort}:${spec.port}`,
|
|
119
|
-
...(applied.extraPorts ?? []).map((p) =>
|
|
118
|
+
...(applied.extraPorts ?? []).map((p) => {
|
|
119
|
+
const hostPort = options.instanceId
|
|
120
|
+
? extraHostPortForInstance(spec.role, p.host, options.instanceId)
|
|
121
|
+
: p.host;
|
|
122
|
+
return `${hostPort}:${p.container}`;
|
|
123
|
+
}),
|
|
120
124
|
];
|
|
121
125
|
const service: Record<string, unknown> = {
|
|
122
126
|
image: spec.image,
|
|
@@ -126,6 +130,7 @@ export function emitComposeLayers(
|
|
|
126
130
|
};
|
|
127
131
|
if (applied.environment) service.environment = applied.environment;
|
|
128
132
|
if (applied.command) service.command = applied.command;
|
|
133
|
+
if (applied.entrypoint) service.entrypoint = applied.entrypoint;
|
|
129
134
|
if (applied.healthcheck) service.healthcheck = applied.healthcheck;
|
|
130
135
|
if (applied.volumes) service.volumes = applied.volumes;
|
|
131
136
|
if (applied.user) service.user = applied.user;
|
|
@@ -194,13 +199,12 @@ export function buildStackEnv(
|
|
|
194
199
|
specs: readonly ServiceSpec[],
|
|
195
200
|
recipes: readonly ImageRecipe[] = [],
|
|
196
201
|
host = "127.0.0.1",
|
|
202
|
+
controls: Readonly<Record<string, string>> = {},
|
|
203
|
+
instanceId?: string,
|
|
197
204
|
): Record<string, string> {
|
|
198
205
|
const env: Record<string, string> = {};
|
|
199
206
|
for (const spec of specs) {
|
|
200
207
|
const prefix = envPrefix(spec.role);
|
|
201
|
-
env[`${prefix}_USER`] = spec.credentials.user;
|
|
202
|
-
env[`${prefix}_PASSWORD`] = spec.credentials.password;
|
|
203
|
-
env[`${prefix}_DB`] = spec.credentials.database;
|
|
204
208
|
const recipe = recipeFor(spec.image, recipes);
|
|
205
209
|
const url = recipe.url(spec, {
|
|
206
210
|
host,
|
|
@@ -209,10 +213,53 @@ export function buildStackEnv(
|
|
|
209
213
|
password: spec.credentials.password,
|
|
210
214
|
database: spec.credentials.database,
|
|
211
215
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const applied = recipe.apply(spec);
|
|
217
|
+
const uiExtra = applied.extraPorts?.[0];
|
|
218
|
+
const uiHost =
|
|
219
|
+
uiExtra === undefined
|
|
220
|
+
? undefined
|
|
221
|
+
: instanceId
|
|
222
|
+
? extraHostPortForInstance(spec.role, uiExtra.host, instanceId)
|
|
223
|
+
: uiExtra.host;
|
|
224
|
+
if (spec.role === "store.sql") {
|
|
225
|
+
env[`${prefix}_USER`] = spec.credentials.user;
|
|
226
|
+
env[`${prefix}_PASSWORD`] = spec.credentials.password;
|
|
227
|
+
env[`${prefix}_DB`] = spec.credentials.database;
|
|
228
|
+
env[`${prefix}_URL`] = url;
|
|
229
|
+
env.DATABASE_URL = url;
|
|
230
|
+
} else if (spec.role === "store.kv") {
|
|
231
|
+
env[`${prefix}_PASSWORD`] = spec.credentials.password;
|
|
232
|
+
env[`${prefix}_URL`] = url;
|
|
233
|
+
env.REDIS_URL = url;
|
|
234
|
+
} else if (spec.role === "store.files") {
|
|
235
|
+
env[`${prefix}_URL`] = url;
|
|
236
|
+
env.S3_ACCESS_KEY_ID = spec.credentials.user;
|
|
237
|
+
env.S3_SECRET_ACCESS_KEY = spec.credentials.password;
|
|
238
|
+
env.S3_BUCKET = spec.credentials.database;
|
|
239
|
+
env.S3_URL = url;
|
|
240
|
+
env.S3_ENDPOINT = new URL(url).origin;
|
|
241
|
+
env.S3_REGION = "us-east-1";
|
|
242
|
+
if (uiHost !== undefined) env.S3_CONSOLE_URL = `http://${host}:${uiHost}`;
|
|
243
|
+
} else if (spec.role === "channel.email") {
|
|
244
|
+
env[`${prefix}_URL`] = url;
|
|
245
|
+
env.SMTP_URL = url;
|
|
246
|
+
env.SMTP_HOST = host;
|
|
247
|
+
env.SMTP_PORT = String(spec.hostPort);
|
|
248
|
+
if (uiHost !== undefined) env.MAILPIT_UI_URL = `http://${host}:${uiHost}`;
|
|
249
|
+
} else if (spec.role === "vault") {
|
|
250
|
+
// Token is minted by the bootstrap — never a generated password here.
|
|
251
|
+
env[`${prefix}_URL`] = url;
|
|
252
|
+
env.OKE_VAULT_URL = url;
|
|
253
|
+
} else {
|
|
254
|
+
env[`${prefix}_USER`] = spec.credentials.user;
|
|
255
|
+
env[`${prefix}_PASSWORD`] = spec.credentials.password;
|
|
256
|
+
env[`${prefix}_DB`] = spec.credentials.database;
|
|
257
|
+
env[`${prefix}_URL`] = url;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const key of STACK_CONTROL_KEYS) {
|
|
261
|
+
const value = controls[key];
|
|
262
|
+
if (value !== undefined) env[key] = value;
|
|
216
263
|
}
|
|
217
264
|
return env;
|
|
218
265
|
}
|
|
@@ -225,12 +272,48 @@ const ROLE_SECTION_TITLE: Readonly<Record<string, string>> = {
|
|
|
225
272
|
"store.index": "store.index — search index",
|
|
226
273
|
"channel.email": "channel.email — Mailpit (SMTP + UI)",
|
|
227
274
|
signal: "signal — message bus",
|
|
275
|
+
vault: "vault — OpenBao",
|
|
228
276
|
};
|
|
229
277
|
|
|
230
278
|
/** Friendly aliases emitted beside their role block. */
|
|
231
279
|
const ROLE_ALIASES: Readonly<Record<string, readonly string[]>> = {
|
|
232
|
-
"store.sql": ["DATABASE_URL"],
|
|
233
|
-
"store.kv": ["REDIS_URL"],
|
|
280
|
+
"store.sql": ["DATABASE_URL", "PGDATA", "POSTGRES_INITDB_ARGS"],
|
|
281
|
+
"store.kv": ["REDIS_URL", "OKE_STORE_KV_MAXMEMORY", "OKE_STORE_KV_MAXMEMORY_POLICY"],
|
|
282
|
+
"store.files": [
|
|
283
|
+
"S3_ACCESS_KEY_ID",
|
|
284
|
+
"S3_SECRET_ACCESS_KEY",
|
|
285
|
+
"S3_BUCKET",
|
|
286
|
+
"S3_URL",
|
|
287
|
+
"S3_ENDPOINT",
|
|
288
|
+
"S3_CONSOLE_URL",
|
|
289
|
+
"S3_REGION",
|
|
290
|
+
"S3_SESSION_TOKEN",
|
|
291
|
+
],
|
|
292
|
+
"channel.email": [
|
|
293
|
+
"SMTP_URL",
|
|
294
|
+
"SMTP_HOST",
|
|
295
|
+
"SMTP_PORT",
|
|
296
|
+
"MAILPIT_UI_URL",
|
|
297
|
+
"SMTP_USER",
|
|
298
|
+
"SMTP_PASSWORD",
|
|
299
|
+
"MP_MAX_MESSAGES",
|
|
300
|
+
"MP_SMTP_AUTH_ACCEPT_ANY",
|
|
301
|
+
"MP_SMTP_AUTH_ALLOW_INSECURE",
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** Optional controls documented in `.env.docker` and preserved on regeneration. */
|
|
306
|
+
const ROLE_CONTROL_EXAMPLES: Readonly<Record<string, readonly string[]>> = {
|
|
307
|
+
"store.sql": ["PGDATA=/var/lib/postgresql/data/pgdata", "POSTGRES_INITDB_ARGS=--data-checksums"],
|
|
308
|
+
"store.kv": ["OKE_STORE_KV_MAXMEMORY=256mb", "OKE_STORE_KV_MAXMEMORY_POLICY=allkeys-lru"],
|
|
309
|
+
"store.files": ["S3_SESSION_TOKEN="],
|
|
310
|
+
"channel.email": [
|
|
311
|
+
"SMTP_USER=",
|
|
312
|
+
"SMTP_PASSWORD=",
|
|
313
|
+
"MP_MAX_MESSAGES=500",
|
|
314
|
+
"MP_SMTP_AUTH_ACCEPT_ANY=1",
|
|
315
|
+
"MP_SMTP_AUTH_ALLOW_INSECURE=1",
|
|
316
|
+
],
|
|
234
317
|
};
|
|
235
318
|
|
|
236
319
|
/**
|
|
@@ -240,8 +323,14 @@ const ROLE_ALIASES: Readonly<Record<string, readonly string[]>> = {
|
|
|
240
323
|
*/
|
|
241
324
|
function roleFromEnvKey(key: string): string | undefined {
|
|
242
325
|
const m = /^OKE_(.+)_(USER|PASSWORD|DB|URL)$/.exec(key);
|
|
243
|
-
if (
|
|
244
|
-
|
|
326
|
+
if (m) return m[1]!.toLowerCase().replaceAll("_", ".");
|
|
327
|
+
if (key.startsWith("S3_")) return "store.files";
|
|
328
|
+
if (key.startsWith("SMTP_") || key.startsWith("MP_") || key === "MAILPIT_UI_URL") {
|
|
329
|
+
return "channel.email";
|
|
330
|
+
}
|
|
331
|
+
if (key === "PGDATA" || key === "POSTGRES_INITDB_ARGS") return "store.sql";
|
|
332
|
+
if (key.startsWith("OKE_STORE_KV_MAXMEMORY")) return "store.kv";
|
|
333
|
+
return undefined;
|
|
245
334
|
}
|
|
246
335
|
|
|
247
336
|
/**
|
|
@@ -272,8 +361,7 @@ export function formatStackEnv(env: Readonly<Record<string, string>>): string {
|
|
|
272
361
|
const prefix = envPrefix(role);
|
|
273
362
|
const title = ROLE_SECTION_TITLE[role] ?? role;
|
|
274
363
|
lines.push(`# ── ${title} ${"─".repeat(Math.max(4, 56 - title.length))}`);
|
|
275
|
-
for (const
|
|
276
|
-
const key = `${prefix}_${field}`;
|
|
364
|
+
for (const key of [`${prefix}_USER`, `${prefix}_PASSWORD`, `${prefix}_DB`, `${prefix}_URL`]) {
|
|
277
365
|
const value = env[key];
|
|
278
366
|
if (value === undefined) continue;
|
|
279
367
|
lines.push(`${key}=${escapeEnv(value)}`);
|
|
@@ -285,6 +373,14 @@ export function formatStackEnv(env: Readonly<Record<string, string>>): string {
|
|
|
285
373
|
lines.push(`${alias}=${escapeEnv(value)}`);
|
|
286
374
|
used.add(alias);
|
|
287
375
|
}
|
|
376
|
+
const controls = ROLE_CONTROL_EXAMPLES[role] ?? [];
|
|
377
|
+
const inactive = controls.filter(
|
|
378
|
+
(example) => env[example.slice(0, example.indexOf("="))] === undefined,
|
|
379
|
+
);
|
|
380
|
+
if (inactive.length > 0) {
|
|
381
|
+
lines.push("# Optional controls (uncomment to override; preserved on regeneration):");
|
|
382
|
+
for (const example of inactive) lines.push(`# ${example}`);
|
|
383
|
+
}
|
|
288
384
|
lines.push("");
|
|
289
385
|
}
|
|
290
386
|
|
package/src/docker/derive.ts
CHANGED
|
@@ -54,7 +54,13 @@ export function deriveInfrastructure(options: DeriveOptions): DeriveResult {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const stackEnv = buildStackEnv(
|
|
57
|
+
const stackEnv = buildStackEnv(
|
|
58
|
+
specs,
|
|
59
|
+
normalised.recipes ?? [],
|
|
60
|
+
normalised.host ?? "127.0.0.1",
|
|
61
|
+
normalised.controls,
|
|
62
|
+
normalised.instanceId,
|
|
63
|
+
);
|
|
58
64
|
|
|
59
65
|
return { specs, files, stackEnv, composeFiles };
|
|
60
66
|
}
|
|
@@ -197,6 +197,78 @@ describe("deriveInfrastructure", () => {
|
|
|
197
197
|
expect(formatStackEnv(result.stackEnv)).toContain("OKE_STORE_SQL_PASSWORD=");
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
test("emits protocol-specific env keys plus optional control notes", () => {
|
|
201
|
+
const result = deriveInfrastructure({
|
|
202
|
+
images: {
|
|
203
|
+
"store.sql": "postgres:18-alpine",
|
|
204
|
+
"store.kv": "redis:8-alpine",
|
|
205
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
206
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
207
|
+
},
|
|
208
|
+
credentials: {
|
|
209
|
+
...fixedCreds,
|
|
210
|
+
"store.files": {
|
|
211
|
+
user: "files-key",
|
|
212
|
+
password: "files-secret",
|
|
213
|
+
database: "oke",
|
|
214
|
+
},
|
|
215
|
+
"channel.email": {
|
|
216
|
+
user: "unused",
|
|
217
|
+
password: "unused-secret",
|
|
218
|
+
database: "oke",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
expect(result.stackEnv.S3_ACCESS_KEY_ID).toBe("files-key");
|
|
223
|
+
expect(result.stackEnv.S3_SECRET_ACCESS_KEY).toBe("files-secret");
|
|
224
|
+
expect(result.stackEnv.S3_BUCKET).toBe("oke");
|
|
225
|
+
expect(result.stackEnv.S3_URL).toContain("http://files-key:");
|
|
226
|
+
expect(result.stackEnv.S3_ENDPOINT).toBe("http://127.0.0.1:9000");
|
|
227
|
+
expect(result.stackEnv.S3_CONSOLE_URL).toBe("http://127.0.0.1:9001");
|
|
228
|
+
expect(result.stackEnv.SMTP_URL).toBe("smtp://127.0.0.1:1025");
|
|
229
|
+
expect(result.stackEnv.SMTP_HOST).toBe("127.0.0.1");
|
|
230
|
+
expect(result.stackEnv.SMTP_PORT).toBe("1025");
|
|
231
|
+
expect(result.stackEnv.MAILPIT_UI_URL).toBe("http://127.0.0.1:8025");
|
|
232
|
+
expect(result.stackEnv.OKE_STORE_KV_PASSWORD).toBe(fixedCreds["store.kv"].password);
|
|
233
|
+
expect(result.stackEnv.OKE_STORE_KV_USER).toBeUndefined();
|
|
234
|
+
expect(result.stackEnv.OKE_STORE_KV_DB).toBeUndefined();
|
|
235
|
+
expect(result.stackEnv.OKE_CHANNEL_EMAIL_PASSWORD).toBeUndefined();
|
|
236
|
+
|
|
237
|
+
const text = formatStackEnv(result.stackEnv);
|
|
238
|
+
expect(text).toContain("# OKE_STORE_KV_MAXMEMORY=256mb");
|
|
239
|
+
expect(text).toContain("# S3_SESSION_TOKEN=");
|
|
240
|
+
expect(text).toContain("# SMTP_USER=");
|
|
241
|
+
expect(text).toContain("# POSTGRES_INITDB_ARGS=--data-checksums");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("instance-offset UI aliases land in stack env", () => {
|
|
245
|
+
const id = "a3f791";
|
|
246
|
+
const n = Number.parseInt(id.slice(0, 4), 16) % 1000;
|
|
247
|
+
const result = deriveInfrastructure({
|
|
248
|
+
images: {
|
|
249
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
250
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
251
|
+
},
|
|
252
|
+
instanceId: id,
|
|
253
|
+
credentials: {
|
|
254
|
+
"channel.email": {
|
|
255
|
+
user: "oke",
|
|
256
|
+
password: "unused-mail-password",
|
|
257
|
+
database: "oke",
|
|
258
|
+
},
|
|
259
|
+
"store.files": {
|
|
260
|
+
user: "files-key",
|
|
261
|
+
password: "files-secret",
|
|
262
|
+
database: "oke",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
expect(result.stackEnv.SMTP_PORT).toBe(String(20_000 + n));
|
|
267
|
+
expect(result.stackEnv.MAILPIT_UI_URL).toBe(`http://127.0.0.1:${21_000 + n}`);
|
|
268
|
+
expect(result.stackEnv.S3_ENDPOINT).toBe(`http://127.0.0.1:${18_000 + n}`);
|
|
269
|
+
expect(result.stackEnv.S3_CONSOLE_URL).toBe(`http://127.0.0.1:${19_000 + n}`);
|
|
270
|
+
});
|
|
271
|
+
|
|
200
272
|
test("resolveStack previews without writing", () => {
|
|
201
273
|
const rows = resolveStack({
|
|
202
274
|
images: { "store.sql": "postgres:16" },
|
|
@@ -205,5 +277,36 @@ describe("deriveInfrastructure", () => {
|
|
|
205
277
|
expect(rows).toHaveLength(1);
|
|
206
278
|
expect(rows[0]!.recipe).toBe("postgres");
|
|
207
279
|
expect(rows[0]!.hostPort).toBe(5432);
|
|
280
|
+
expect(rows[0]!.extraPorts).toEqual([]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("resolveStack includes instance-offset Mailpit UI / RustFS console", () => {
|
|
284
|
+
const id = "a3f791";
|
|
285
|
+
const n = Number.parseInt(id.slice(0, 4), 16) % 1000;
|
|
286
|
+
const rows = resolveStack({
|
|
287
|
+
images: {
|
|
288
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
289
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
290
|
+
},
|
|
291
|
+
instanceId: id,
|
|
292
|
+
credentials: {
|
|
293
|
+
"channel.email": {
|
|
294
|
+
user: "oke",
|
|
295
|
+
password: "stack-preview-mail-password",
|
|
296
|
+
database: "oke",
|
|
297
|
+
},
|
|
298
|
+
"store.files": {
|
|
299
|
+
user: "oke",
|
|
300
|
+
password: "stack-preview-files-password",
|
|
301
|
+
database: "oke",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const mail = rows.find((r) => r.role === "channel.email")!;
|
|
306
|
+
const files = rows.find((r) => r.role === "store.files")!;
|
|
307
|
+
expect(mail.hostPort).toBe(20_000 + n);
|
|
308
|
+
expect(mail.extraPorts).toEqual([{ hostPort: 21_000 + n, containerPort: 8025 }]);
|
|
309
|
+
expect(files.hostPort).toBe(18_000 + n);
|
|
310
|
+
expect(files.extraPorts).toEqual([{ hostPort: 19_000 + n, containerPort: 9001 }]);
|
|
208
311
|
});
|
|
209
312
|
});
|
package/src/docker/index.ts
CHANGED
|
@@ -37,12 +37,22 @@ export { emitDockerfile } from "./dockerfile.ts";
|
|
|
37
37
|
export { credEnv, envPrefix, serviceNameFor } from "./helpers.ts";
|
|
38
38
|
export { generateCredentials } from "./credentials.ts";
|
|
39
39
|
export { builtinRecipes, mailpit, postgres, recipeFor, redis, rustfs } from "./recipes/index.ts";
|
|
40
|
-
export { formatStackPreview, resolveStack, type StackRow } from "./stack.ts";
|
|
41
40
|
export {
|
|
41
|
+
formatStackPreview,
|
|
42
|
+
resolveExtraPorts,
|
|
43
|
+
resolveStack,
|
|
44
|
+
type StackExtraPort,
|
|
45
|
+
type StackRow,
|
|
46
|
+
} from "./stack.ts";
|
|
47
|
+
export {
|
|
48
|
+
extraHostPortForInstance,
|
|
42
49
|
hostPortForInstance,
|
|
43
50
|
instancePortOffset,
|
|
51
|
+
loadExistingStackControls,
|
|
44
52
|
loadExistingStackCredentials,
|
|
53
|
+
parseStackControls,
|
|
45
54
|
parseStackCredentials,
|
|
55
|
+
STACK_CONTROL_KEYS,
|
|
46
56
|
stackAppSlug,
|
|
47
57
|
stackInstanceId,
|
|
48
58
|
} from "./stack-id.ts";
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ImageRecipe } from "../types.ts";
|
|
6
6
|
import { mailpit } from "./mailpit.ts";
|
|
7
|
+
import { openbao } from "./openbao.ts";
|
|
7
8
|
import { postgres } from "./postgres.ts";
|
|
8
9
|
import { redis } from "./redis.ts";
|
|
9
10
|
import { rustfs } from "./rustfs.ts";
|
|
10
11
|
|
|
11
12
|
/** Default recipe catalogue. */
|
|
12
|
-
export const builtinRecipes: readonly ImageRecipe[] = [postgres, redis, mailpit, rustfs];
|
|
13
|
+
export const builtinRecipes: readonly ImageRecipe[] = [postgres, redis, mailpit, rustfs, openbao];
|
|
13
14
|
|
|
14
|
-
export { mailpit, postgres, redis, rustfs };
|
|
15
|
+
export { mailpit, openbao, postgres, redis, rustfs };
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Resolve the recipe for an image reference.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenBao image recipe — durable single-node secrets service (Raft storage).
|
|
3
|
+
*
|
|
4
|
+
* Never `-dev`: data lives on a named volume under `/openbao/data`; init +
|
|
5
|
+
* unseal is handled by the CLI bootstrap (keys stay on the host).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ImageRecipe } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Server config passed via `BAO_LOCAL_CONFIG` (official image). Single-node
|
|
12
|
+
* Raft still requires `cluster_addr` / `api_addr` per the OpenBao docs.
|
|
13
|
+
*/
|
|
14
|
+
const OPENBAO_LOCAL_CONFIG = JSON.stringify({
|
|
15
|
+
listener: { tcp: { address: "0.0.0.0:8200", tls_disable: true } },
|
|
16
|
+
// Raft under /openbao/file — the stock entrypoint chowns that dir for the
|
|
17
|
+
// `openbao` user; a root-owned fresh named volume stays writable. Raft does
|
|
18
|
+
// not create nested dirs, so the path must be the chowned dir itself.
|
|
19
|
+
storage: { raft: { path: "/openbao/file", node_id: "oke" } },
|
|
20
|
+
api_addr: "http://0.0.0.0:8200",
|
|
21
|
+
cluster_addr: "http://127.0.0.1:8201",
|
|
22
|
+
disable_mlock: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** OpenBao secrets service. API on 8200. */
|
|
26
|
+
export const openbao: ImageRecipe = {
|
|
27
|
+
id: "openbao",
|
|
28
|
+
port: 8200,
|
|
29
|
+
match: (i) => /openbao/i.test(i),
|
|
30
|
+
apply: (s) => ({
|
|
31
|
+
environment: {
|
|
32
|
+
BAO_LOCAL_CONFIG: OPENBAO_LOCAL_CONFIG,
|
|
33
|
+
},
|
|
34
|
+
command: ["server"],
|
|
35
|
+
volumes: [`${s.serviceName}-data:/openbao/file`],
|
|
36
|
+
healthcheck: {
|
|
37
|
+
// `/sys/init` answers 200 even while sealed — only proves the server
|
|
38
|
+
// process is up; bootstrap owns the real unseal step.
|
|
39
|
+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8200/v1/sys/init >/dev/null 2>&1 || exit 1"],
|
|
40
|
+
interval: "5s",
|
|
41
|
+
timeout: "5s",
|
|
42
|
+
retries: 12,
|
|
43
|
+
start_period: "5s",
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
url: (_s, c) => `http://${c.host}:${c.port}`,
|
|
47
|
+
};
|
|
@@ -11,7 +11,11 @@ export const redis: ImageRecipe = {
|
|
|
11
11
|
port: 6379,
|
|
12
12
|
match: (i) => /redis|valkey|dragonfly|keydb/i.test(i),
|
|
13
13
|
apply: (s) => ({
|
|
14
|
-
command: [
|
|
14
|
+
command: [
|
|
15
|
+
"sh",
|
|
16
|
+
"-c",
|
|
17
|
+
'exec redis-server --requirepass "$$OKE_STORE_KV_PASSWORD" --maxmemory "$${OKE_STORE_KV_MAXMEMORY:-0}" --maxmemory-policy "$${OKE_STORE_KV_MAXMEMORY_POLICY:-noeviction}"',
|
|
18
|
+
],
|
|
15
19
|
healthcheck: {
|
|
16
20
|
test: ["CMD", "redis-cli", "-a", credEnv(s, "PASSWORD"), "ping"],
|
|
17
21
|
interval: "5s",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* RustFS image recipe — S3-compatible object store (Apache 2.0 MinIO alternative).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { credEnv } from "../helpers.ts";
|
|
6
5
|
import type { ImageRecipe } from "../types.ts";
|
|
7
6
|
|
|
8
7
|
/** S3-protocol object storage (RustFS). Console on 9001. */
|
|
@@ -12,8 +11,8 @@ export const rustfs: ImageRecipe = {
|
|
|
12
11
|
match: (i) => /rustfs/i.test(i),
|
|
13
12
|
apply: (s) => ({
|
|
14
13
|
environment: {
|
|
15
|
-
RUSTFS_ACCESS_KEY:
|
|
16
|
-
RUSTFS_SECRET_KEY:
|
|
14
|
+
RUSTFS_ACCESS_KEY: "${S3_ACCESS_KEY_ID}",
|
|
15
|
+
RUSTFS_SECRET_KEY: "${S3_SECRET_ACCESS_KEY}",
|
|
17
16
|
RUSTFS_CONSOLE_ENABLE: "true",
|
|
18
17
|
RUSTFS_ADDRESS: ":9000",
|
|
19
18
|
},
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import {
|
|
7
|
+
extraHostPortForInstance,
|
|
7
8
|
hostPortForInstance,
|
|
8
9
|
instancePortOffset,
|
|
10
|
+
parseStackControls,
|
|
9
11
|
parseStackCredentials,
|
|
10
12
|
stackAppSlug,
|
|
11
13
|
stackInstanceId,
|
|
@@ -29,14 +31,23 @@ describe("stackInstanceId", () => {
|
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
describe("hostPortForInstance", () => {
|
|
32
|
-
test("
|
|
34
|
+
test("assigns built-in roles disjoint per-project ranges", () => {
|
|
33
35
|
const id = "abcd12";
|
|
36
|
+
const n = instancePortOffset(id);
|
|
34
37
|
const sql = hostPortForInstance("store.sql", 5432, id);
|
|
35
38
|
const kv = hostPortForInstance("store.kv", 6379, id);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const files = hostPortForInstance("store.files", 9000, id);
|
|
40
|
+
const filesConsole = extraHostPortForInstance("store.files", 9001, id);
|
|
41
|
+
const email = hostPortForInstance("channel.email", 1025, id);
|
|
42
|
+
const emailUi = extraHostPortForInstance("channel.email", 8025, id);
|
|
43
|
+
expect([sql, kv, files, filesConsole, email, emailUi]).toEqual([
|
|
44
|
+
15_000 + n,
|
|
45
|
+
16_000 + n,
|
|
46
|
+
18_000 + n,
|
|
47
|
+
19_000 + n,
|
|
48
|
+
20_000 + n,
|
|
49
|
+
21_000 + n,
|
|
50
|
+
]);
|
|
40
51
|
});
|
|
41
52
|
});
|
|
42
53
|
|
|
@@ -54,6 +65,26 @@ OKE_STORE_KV_DB=oke
|
|
|
54
65
|
expect(creds["store.sql"]?.password).toBe("s3cret");
|
|
55
66
|
expect(creds["store.kv"]?.password).toBe("kvpass");
|
|
56
67
|
});
|
|
68
|
+
|
|
69
|
+
test("reads service-specific S3 credentials and optional controls", () => {
|
|
70
|
+
const text = `
|
|
71
|
+
S3_ACCESS_KEY_ID=files-key
|
|
72
|
+
S3_SECRET_ACCESS_KEY=files-secret
|
|
73
|
+
S3_REGION=eu-central-1
|
|
74
|
+
OKE_STORE_KV_MAXMEMORY=512mb
|
|
75
|
+
# SMTP_USER=commented-out
|
|
76
|
+
`;
|
|
77
|
+
const creds = parseStackCredentials(text, ["store.files"]);
|
|
78
|
+
expect(creds["store.files"]).toEqual({
|
|
79
|
+
user: "files-key",
|
|
80
|
+
password: "files-secret",
|
|
81
|
+
database: "oke",
|
|
82
|
+
});
|
|
83
|
+
expect(parseStackControls(text)).toEqual({
|
|
84
|
+
OKE_STORE_KV_MAXMEMORY: "512mb",
|
|
85
|
+
S3_REGION: "eu-central-1",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
57
88
|
});
|
|
58
89
|
|
|
59
90
|
describe("deriveInfrastructure instanceId", () => {
|
|
@@ -86,7 +117,8 @@ describe("deriveInfrastructure instanceId", () => {
|
|
|
86
117
|
});
|
|
87
118
|
|
|
88
119
|
test("offsets Mailpit UI and RustFS console so stacks do not share 8025/9001", () => {
|
|
89
|
-
|
|
120
|
+
// Regression: offset 975 previously mapped Mailpit's 8025 UI onto RustFS's 9000 API.
|
|
121
|
+
const id = "a3f791";
|
|
90
122
|
const n = instancePortOffset(id);
|
|
91
123
|
const result = deriveInfrastructure({
|
|
92
124
|
images: {
|
|
@@ -111,9 +143,12 @@ describe("deriveInfrastructure instanceId", () => {
|
|
|
111
143
|
});
|
|
112
144
|
const mailYml = result.files.find((f) => f.path === "compose.channel.email.yml")!.content;
|
|
113
145
|
const filesYml = result.files.find((f) => f.path === "compose.store.files.yml")!.content;
|
|
114
|
-
expect(mailYml).toContain(`${
|
|
146
|
+
expect(mailYml).toContain(`${20_000 + n}:1025`);
|
|
147
|
+
expect(mailYml).toContain(`${21_000 + n}:8025`);
|
|
115
148
|
expect(mailYml).not.toContain("8025:8025");
|
|
116
|
-
expect(filesYml).toContain(`${
|
|
149
|
+
expect(filesYml).toContain(`${18_000 + n}:9000`);
|
|
150
|
+
expect(filesYml).toContain(`${19_000 + n}:9001`);
|
|
117
151
|
expect(filesYml).not.toContain("9001:9001");
|
|
152
|
+
expect(mailYml).not.toContain("9000:8025");
|
|
118
153
|
});
|
|
119
154
|
});
|