okengine 0.10.2 → 0.11.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/package.json +76 -10
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/deployment/docker-swarm.mdx +48 -111
- package/site/content/docs/deployment/docker.mdx +43 -36
- package/site/content/docs/deployment/index.mdx +9 -9
- package/site/content/docs/deployment/kubernetes.mdx +4 -4
- package/site/content/docs/elements/ai.mdx +7 -13
- package/site/content/docs/elements/channel.mdx +9 -13
- package/site/content/docs/elements/clock.mdx +8 -14
- package/site/content/docs/elements/flow.mdx +46 -54
- package/site/content/docs/elements/gate.mdx +6 -11
- package/site/content/docs/elements/signal.mdx +2 -8
- package/site/content/docs/elements/store.mdx +79 -93
- package/site/content/docs/elements/vault.mdx +89 -76
- package/site/content/docs/get-started/basic-usage.mdx +8 -14
- package/site/content/docs/get-started/installation.mdx +36 -36
- package/site/content/docs/get-started/introduction.mdx +11 -0
- package/site/content/docs/get-started/why.mdx +3 -4
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +11 -11
- package/site/content/docs/plugins/otp.mdx +2 -2
- package/site/content/docs/plugins/passkey.mdx +1 -1
- package/site/content/docs/plugins/two-factor.mdx +1 -1
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +1 -1
- package/site/content/docs/recipes/cockroachdb.mdx +2 -2
- package/site/content/docs/recipes/dragonfly.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -2
- package/site/content/docs/recipes/llama-cpp.mdx +21 -23
- package/site/content/docs/recipes/mailpit.mdx +11 -11
- package/site/content/docs/recipes/meilisearch.mdx +10 -10
- package/site/content/docs/recipes/meta.json +0 -1
- package/site/content/docs/recipes/ollama.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +12 -9
- package/site/content/docs/recipes/postgres.mdx +3 -3
- package/site/content/docs/recipes/redis.mdx +2 -2
- package/site/content/docs/recipes/rustfs.mdx +10 -7
- package/site/content/docs/recipes/sglang.mdx +1 -1
- package/site/content/docs/recipes/supabase-docker.mdx +1 -1
- package/site/content/docs/recipes/timescale.mdx +2 -2
- package/site/content/docs/recipes/valkey.mdx +1 -1
- package/site/content/docs/recipes/vllm.mdx +1 -1
- package/site/content/docs/recipes/yugabytedb.mdx +2 -2
- package/site/content/docs/reference/cli.md +61 -38
- package/site/content/docs/reference/configuration.mdx +145 -70
- package/site/content/docs/reference/environment-variables.mdx +36 -28
- package/site/content/docs/reference/errors.mdx +28 -29
- package/site/content/docs/reference/fx.mdx +14 -7
- package/site/content/docs/reference/i18n.mdx +17 -6
- package/site/content/docs/reference/index.mdx +5 -0
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/migrating-environments.mdx +158 -0
- package/site/content/docs/reference/plugins.mdx +2 -2
- package/src/auth/gate-auth.test.ts +12 -10
- package/src/cli/ai-setup/ai-setup.test.ts +30 -16
- package/src/cli/ai-setup/apply.ts +111 -15
- package/src/cli/ai-setup/catalog.ts +3 -3
- package/src/cli/ai-setup/index.ts +1 -1
- package/src/cli/ai-setup/prompts.ts +1 -1
- package/src/cli/ai-setup/recommend.test.ts +1 -1
- package/src/cli/ask-seed.test.ts +3 -3
- package/src/cli/build.ts +8 -9
- package/src/cli/db-auto-push.test.ts +6 -3
- package/src/cli/db-auto-push.ts +7 -5
- package/src/cli/db-seed.ts +7 -8
- package/src/cli/db.test.ts +70 -71
- package/src/cli/db.ts +86 -12
- package/src/cli/dev-controller.test.ts +35 -0
- package/src/cli/dev-controller.ts +298 -0
- package/src/cli/dev-controls.ts +11 -65
- package/src/cli/dev-db-push.test.ts +47 -15
- package/src/cli/dev-schema-sync.test.ts +11 -11
- package/src/cli/dev-schema-sync.ts +10 -10
- package/src/cli/dev-session-lock.test.ts +106 -0
- package/src/cli/dev-session-lock.ts +209 -0
- package/src/cli/dev.test.ts +246 -114
- package/src/cli/dev.ts +153 -205
- package/src/cli/doc-staleness.test.ts +3 -6
- package/src/cli/docker-clean.test.ts +33 -1
- package/src/cli/docker-clean.ts +28 -33
- package/src/cli/docker-cli.test.ts +12 -11
- package/src/cli/docker.ts +95 -20
- package/src/cli/doctor.test.ts +40 -0
- package/src/cli/doctor.ts +142 -9
- package/src/cli/drizzle-env.test.ts +11 -13
- package/src/cli/drizzle-env.ts +18 -11
- package/src/cli/ensure-drizzle-config.ts +11 -14
- package/src/cli/hero-meta.test.ts +15 -15
- package/src/cli/hero-meta.ts +10 -9
- package/src/cli/index.ts +36 -6
- package/src/cli/load-config.images.test.ts +44 -10
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +35 -29
- package/src/cli/meilisearch-local.ts +1 -2
- package/src/cli/mode.ts +7 -58
- package/src/cli/registry.ts +175 -19
- package/src/cli/resolve-dev-sql-env.test.ts +8 -16
- package/src/cli/resolve-dev-sql-env.ts +13 -15
- package/src/cli/safe-defaults.test.ts +29 -11
- package/src/cli/schema.ts +65 -16
- package/src/cli/test.ts +58 -0
- package/src/cli/tui/App.tsx +252 -0
- package/src/cli/tui/DevLive.tsx +116 -0
- package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
- package/src/cli/tui/components/HeaderBar.tsx +67 -0
- package/src/cli/tui/components/LogStream.tsx +47 -0
- package/src/cli/tui/components/PanelCard.tsx +39 -0
- package/src/cli/tui/components/SelectRow.tsx +37 -0
- package/src/cli/tui/components/SlashPalette.tsx +172 -0
- package/src/cli/tui/components/Spinner.tsx +32 -0
- package/src/cli/tui/components/StatusBar.tsx +51 -0
- package/src/cli/tui/components/StatusDot.tsx +37 -0
- package/src/cli/tui/components/TabBar.tsx +49 -0
- package/src/cli/tui/components/Wordmark.tsx +43 -0
- package/src/cli/tui/format.ts +38 -0
- package/src/cli/tui/keys.test.ts +18 -0
- package/src/cli/tui/keys.ts +26 -0
- package/src/cli/tui/launch.test.ts +18 -0
- package/src/cli/tui/launch.ts +57 -0
- package/src/cli/tui/panels/Dashboard.tsx +201 -0
- package/src/cli/tui/panels/Database.tsx +198 -0
- package/src/cli/tui/panels/Dev.tsx +257 -0
- package/src/cli/tui/panels/Docker.tsx +357 -0
- package/src/cli/tui/panels/Navigator.test.ts +22 -0
- package/src/cli/tui/panels/Navigator.tsx +213 -0
- package/src/cli/tui/slash-catalog.test.ts +31 -0
- package/src/cli/tui/slash-catalog.ts +184 -0
- package/src/cli/tui/slash-run.ts +54 -0
- package/src/cli/tui/state/ports.ts +95 -0
- package/src/cli/tui/theme.ts +24 -0
- package/src/cli/vault-cmd.test.ts +402 -0
- package/src/cli/vault-cmd.ts +727 -69
- package/src/cli/vault-secure-input.test.ts +56 -0
- package/src/cli/vault-secure-input.ts +124 -0
- package/src/compiler/effects-infer.ts +7 -0
- package/src/compiler/extract.ts +12 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/generate-adopt.ts +27 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +115 -0
- package/src/config/driver-defaults.ts +156 -0
- package/src/config/index.ts +246 -131
- package/src/config/resolve-driver.test.ts +14 -19
- package/src/console/server/app.ts +4 -2
- package/src/console/server/console.test.ts +58 -1
- package/src/console/server/flows.ts +22 -2
- package/src/console/server/index.ts +3 -0
- package/src/console/server/operator-db.test.ts +55 -13
- package/src/console/server/operator-db.ts +305 -263
- package/src/console/server/serve.ts +16 -7
- package/src/console/server/state.ts +31 -9
- package/src/console/server/vault.test.ts +120 -5
- package/src/console/server/vault.ts +208 -12
- package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
- package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
- package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/shell/client.ts +16 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
- package/src/console/ui/vault/VaultA11yView.tsx +33 -1
- package/src/console/ui/vault/backend.test.ts +75 -0
- package/src/console/ui/vault/backend.ts +117 -0
- package/src/console/ui/vault/fixture.ts +21 -2
- package/src/console/ui/vault/index.ts +11 -1
- package/src/console/ui/vault/types.ts +26 -0
- package/src/docker/compose.ts +164 -61
- package/src/docker/derive.ts +106 -12
- package/src/docker/docker.test.ts +156 -110
- package/src/docker/helpers.ts +98 -0
- package/src/docker/images-config.test.ts +163 -0
- package/src/docker/index.ts +30 -5
- package/src/docker/recipes/index.ts +0 -3
- package/src/docker/recipes/llama-cpp.ts +4 -4
- package/src/docker/recipes/pgdog.ts +9 -2
- package/src/docker/resources.test.ts +71 -0
- package/src/docker/resources.ts +176 -0
- package/src/docker/stack-id.test.ts +11 -4
- package/src/docker/stack-id.ts +42 -32
- package/src/docker/stack.integration.test.ts +4 -15
- package/src/docker/types.ts +35 -3
- package/src/drivers/ai-ollama.integration.test.ts +2 -2
- package/src/drivers/conformance.test.ts +11 -12
- package/src/drivers/drizzle-dialect.test.ts +0 -4
- package/src/drivers/drizzle-dialect.ts +7 -13
- package/src/drivers/index.ts +24 -7
- package/src/drivers/journal-postgres.test.ts +3 -1
- package/src/drivers/journal-postgres.ts +4 -2
- package/src/drivers/meilisearch.ts +1 -1
- package/src/drivers/pglite.ts +28 -5
- package/src/drivers/types.ts +9 -11
- package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
- package/src/drivers/vault-aws-secrets-manager.ts +333 -0
- package/src/drivers/vault-builtin.test.ts +110 -0
- package/src/drivers/vault-builtin.ts +184 -0
- package/src/drivers/vault-driver-removal.test.ts +13 -0
- package/src/drivers/vault-managed.test.ts +95 -0
- package/src/drivers/vault-managed.ts +101 -25
- package/src/drivers/vault-types.ts +25 -5
- package/src/elements/ai/runtime.ts +1 -1
- package/src/elements/clock/chaos.test.ts +4 -1
- package/src/elements/clock/durable.ts +57 -4
- package/src/elements/clock.test.ts +13 -14
- package/src/elements/gate/constants.ts +18 -0
- package/src/elements/gate.test.ts +1 -1
- package/src/elements/gate.ts +2 -2
- package/src/elements/store/domain-ddl.test.ts +5 -12
- package/src/elements/store/emit-drizzle.ts +44 -19
- package/src/elements/store/index-boot.test.ts +15 -116
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/resource-list-docs.fixture.ts +12 -6
- package/src/elements/store/resource-list-docs.test.ts +64 -44
- package/src/elements/store/resource.test.ts +17 -10
- package/src/elements/store/runtime.ts +9 -12
- package/src/elements/store/schema-decl.test.ts +16 -32
- package/src/elements/store/seed.test.ts +1 -2
- package/src/elements/store/seed.ts +7 -8
- package/src/elements/store/sql-condition.test.ts +2 -2
- package/src/elements/store/sql-session.test.ts +8 -10
- package/src/elements/store/sql-session.ts +10 -2
- package/src/elements/store.test.ts +13 -13
- package/src/elements/vault/audit.ts +233 -0
- package/src/elements/vault/boot-chain.test.ts +78 -0
- package/src/elements/vault/boot-chain.ts +43 -39
- package/src/elements/vault/builtin-adapter.test.ts +479 -0
- package/src/elements/vault/builtin-adapter.ts +1184 -0
- package/src/elements/vault/crypto.test.ts +237 -0
- package/src/elements/vault/crypto.ts +431 -0
- package/src/elements/vault/declare.ts +164 -3
- package/src/elements/vault/errors.ts +56 -0
- package/src/elements/vault/kms-unseal.test.ts +76 -0
- package/src/elements/vault/kms-unseal.ts +181 -0
- package/src/elements/vault/path.ts +90 -0
- package/src/elements/vault/resilience.test.ts +102 -0
- package/src/elements/vault/resilience.ts +107 -0
- package/src/elements/vault/runtime.ts +36 -4
- package/src/elements/vault/security-checklist.test.ts +130 -0
- package/src/elements/vault/storage.ts +448 -0
- package/src/elements/vault/types.ts +288 -0
- package/src/elements/vault/unseal.ts +150 -0
- package/src/elements/vault.test.ts +72 -4
- package/src/elements/vault.ts +91 -2
- package/src/full.ts +248 -0
- package/src/http.ts +41 -0
- package/src/i18n/interpolate-simple.ts +37 -0
- package/src/i18n/messages.ts +51 -4
- package/src/i18n-entry.ts +7 -0
- package/src/index.ts +35 -145
- package/src/journal-entry.ts +20 -0
- package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
- package/src/kernel/app.ts +40 -42
- package/src/kernel/auto-registry.test.ts +2 -2
- package/src/kernel/boot-bind/ai.test.ts +5 -5
- package/src/kernel/boot-bind/ai.ts +2 -1
- package/src/kernel/boot-bind/channel.test.ts +6 -6
- package/src/kernel/boot-bind/channel.ts +5 -4
- package/src/kernel/boot-bind/clock.ts +5 -5
- package/src/kernel/boot-bind/gate.ts +6 -7
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +5 -5
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +55 -37
- package/src/kernel/boot-bind/store.ts +24 -34
- package/src/kernel/boot-bind/vault.ts +19 -4
- package/src/kernel/boot.test.ts +12 -4
- package/src/kernel/boot.ts +31 -19
- package/src/kernel/budget-entry.ts +1 -1
- package/src/kernel/compensate.test.ts +288 -1
- package/src/kernel/compensate.ts +126 -0
- package/src/kernel/concurrency.test.ts +10 -9
- package/src/kernel/effects-stamping.test.ts +27 -2
- package/src/kernel/element-registries.ts +5 -3
- package/src/kernel/flow.ts +4 -2
- package/src/kernel/fx.test.ts +119 -3
- package/src/kernel/fx.ts +215 -40
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +130 -7
- package/src/kernel/pipeline.test.ts +20 -6
- package/src/kernel/redacted.test.ts +35 -0
- package/src/kernel/redacted.ts +24 -2
- package/src/kernel/router/create-default.ts +16 -0
- package/src/kernel/router/create-edge.ts +18 -0
- package/src/kernel/router/create.ts +23 -0
- package/src/kernel/router/index.ts +23 -0
- package/src/kernel/router/linear.ts +88 -0
- package/src/kernel/router/regexp.ts +188 -0
- package/src/kernel/router/smart.ts +120 -0
- package/src/kernel/router/trie.ts +108 -0
- package/src/kernel/router/types.ts +56 -0
- package/src/kernel/router.ts +18 -532
- package/src/kernel-entry.ts +43 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/plugins/config-source.test.ts +7 -7
- package/src/release/build-lib.ts +58 -0
- package/src/release/exports.test.ts +4 -2
- package/src/release/exports.ts +30 -4
- package/src/release/measure.ts +101 -22
- package/src/runs/duckdb.ts +7 -2
- package/src/shared/optional-peer.ts +41 -0
- package/src/term.test.ts +3 -44
- package/src/test/create-test-app.ts +12 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/test/reset-element-registries.ts +2 -0
- package/src/upgrade/codemods.test.ts +7 -20
- package/src/upgrade/codemods.ts +93 -60
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
- package/site/content/docs/recipes/openbao.mdx +0 -141
- package/src/cli/ask-dev-mode.ts +0 -40
- package/src/cli/dev-mode.test.ts +0 -45
- package/src/cli/dev-mode.ts +0 -78
- package/src/cli/openbao-bootstrap.test.ts +0 -150
- package/src/cli/openbao-bootstrap.ts +0 -321
- package/src/cli/openbao-restart.integration.test.ts +0 -145
- package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
- package/src/docker/recipes/openbao.ts +0 -47
- package/src/drivers/libsql.ts +0 -179
- package/src/drivers/sqlite.ts +0 -44
- package/src/drivers/vault-openbao.test.ts +0 -97
- package/src/drivers/vault-openbao.ts +0 -143
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ImagesConfig` (config surface, nested) → internal flat role map.
|
|
3
|
+
*
|
|
4
|
+
* Two guarantees:
|
|
5
|
+
* 1. Every built-in image recipe is reachable through the nested shape,
|
|
6
|
+
* correctly placed under its owning element (or flat, for roles with no
|
|
7
|
+
* driver counterpart) — the type-safety gap this closes.
|
|
8
|
+
* 2. A nested config produces byte-identical `deriveInfrastructure()` output
|
|
9
|
+
* (Dockerfile / compose / stack env) to the flat-shape equivalent it
|
|
10
|
+
* replaces — nesting is a config-surface change only, never an internal
|
|
11
|
+
* representation change.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { flattenImagesConfig, type ImagesConfig } from "../config/index.ts";
|
|
16
|
+
import { deriveInfrastructure } from "./derive.ts";
|
|
17
|
+
import { builtinRecipes, recipeFor } from "./recipes/index.ts";
|
|
18
|
+
import type { ServiceCredentials } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
describe("ImagesConfig nesting — recipe coverage", () => {
|
|
21
|
+
test("every built-in recipe id is reachable, correctly nested/flat by role", () => {
|
|
22
|
+
const cases: ReadonlyArray<{ images: ImagesConfig; role: string; image: string }> = [
|
|
23
|
+
// store.sql — postgres-family
|
|
24
|
+
{
|
|
25
|
+
images: { store: { sql: "postgres:18-alpine" } },
|
|
26
|
+
role: "store.sql",
|
|
27
|
+
image: "postgres:18-alpine",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
images: { store: { sql: "cockroachdb/cockroach:v24.1.0" } },
|
|
31
|
+
role: "store.sql",
|
|
32
|
+
image: "cockroachdb/cockroach:v24.1.0",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
images: { store: { sql: "yugabytedb/yugabyte:2024.1.0.0-b1" } },
|
|
36
|
+
role: "store.sql",
|
|
37
|
+
image: "yugabytedb/yugabyte:2024.1.0.0-b1",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
images: { store: { sql: "timescale/timescaledb:2.15.0-pg16" } },
|
|
41
|
+
role: "store.sql",
|
|
42
|
+
image: "timescale/timescaledb:2.15.0-pg16",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
images: { store: { sql: "supabase/postgres:15.1.0.117" } },
|
|
46
|
+
role: "store.sql",
|
|
47
|
+
image: "supabase/postgres:15.1.0.117",
|
|
48
|
+
},
|
|
49
|
+
// store.kv — redis-family
|
|
50
|
+
{ images: { store: { kv: "redis:8-alpine" } }, role: "store.kv", image: "redis:8-alpine" },
|
|
51
|
+
{
|
|
52
|
+
images: { store: { kv: "valkey/valkey:8-alpine" } },
|
|
53
|
+
role: "store.kv",
|
|
54
|
+
image: "valkey/valkey:8-alpine",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
images: { store: { kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0" } },
|
|
58
|
+
role: "store.kv",
|
|
59
|
+
image: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0",
|
|
60
|
+
},
|
|
61
|
+
// store.files / store.index
|
|
62
|
+
{
|
|
63
|
+
images: { store: { files: "rustfs/rustfs:1.0.0-beta.11" } },
|
|
64
|
+
role: "store.files",
|
|
65
|
+
image: "rustfs/rustfs:1.0.0-beta.11",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
images: { store: { index: "getmeili/meilisearch:v1.37" } },
|
|
69
|
+
role: "store.index",
|
|
70
|
+
image: "getmeili/meilisearch:v1.37",
|
|
71
|
+
},
|
|
72
|
+
// channel.email
|
|
73
|
+
{
|
|
74
|
+
images: { channel: { email: "axllent/mailpit:v1.22.3" } },
|
|
75
|
+
role: "channel.email",
|
|
76
|
+
image: "axllent/mailpit:v1.22.3",
|
|
77
|
+
},
|
|
78
|
+
// flat roles — no driver counterpart in DriversConfig
|
|
79
|
+
{
|
|
80
|
+
images: { ai: "ghcr.io/ggml-org/llama.cpp:server-b10290" },
|
|
81
|
+
role: "ai",
|
|
82
|
+
image: "ghcr.io/ggml-org/llama.cpp:server-b10290",
|
|
83
|
+
},
|
|
84
|
+
{ images: { ai: "vllm/vllm-openai:v0.26.0" }, role: "ai", image: "vllm/vllm-openai:v0.26.0" },
|
|
85
|
+
{
|
|
86
|
+
images: { ai: "lmsysorg/sglang:v0.5.16-runtime" },
|
|
87
|
+
role: "ai",
|
|
88
|
+
image: "lmsysorg/sglang:v0.5.16-runtime",
|
|
89
|
+
},
|
|
90
|
+
{ images: { ai: "ollama/ollama:0.32.6" }, role: "ai", image: "ollama/ollama:0.32.6" },
|
|
91
|
+
{
|
|
92
|
+
images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51" },
|
|
93
|
+
role: "pgdog",
|
|
94
|
+
image: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
95
|
+
},
|
|
96
|
+
{ images: { proxy: "caddy:2-alpine" }, role: "proxy", image: "caddy:2-alpine" },
|
|
97
|
+
{ images: { proxy: "traefik:v3.1" }, role: "proxy", image: "traefik:v3.1" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const matchedIds = new Set<string>();
|
|
101
|
+
for (const { images, role, image } of cases) {
|
|
102
|
+
// The nested literal flattens to exactly the dotted/flat role key the
|
|
103
|
+
// rest of the pipeline (buildSpecs, credentials, env prefixes) expects.
|
|
104
|
+
expect(flattenImagesConfig(images)).toEqual({ [role]: image });
|
|
105
|
+
matchedIds.add(recipeFor(image).id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(matchedIds).toEqual(new Set(builtinRecipes.map((r) => r.id)));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("ImagesConfig nesting — compose output parity", () => {
|
|
113
|
+
test("nested config derives byte-identical output to its flat-shape equivalent", () => {
|
|
114
|
+
const flatImages: Readonly<Record<string, string>> = {
|
|
115
|
+
"store.sql": "postgres:18-alpine",
|
|
116
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
117
|
+
"store.kv": "redis:8-alpine",
|
|
118
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
119
|
+
"store.index": "getmeili/meilisearch:v1.37",
|
|
120
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const nestedImages: ImagesConfig = {
|
|
124
|
+
store: {
|
|
125
|
+
sql: "postgres:18-alpine",
|
|
126
|
+
kv: "redis:8-alpine",
|
|
127
|
+
files: "rustfs/rustfs:1.0.0-beta.11",
|
|
128
|
+
index: "getmeili/meilisearch:v1.37",
|
|
129
|
+
},
|
|
130
|
+
channel: { email: "axllent/mailpit:v1.22.3" },
|
|
131
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Same nested→flat set, written in a different field order — proves the
|
|
135
|
+
// parity holds independent of source object key order.
|
|
136
|
+
expect(flattenImagesConfig(nestedImages)).toEqual(flatImages);
|
|
137
|
+
|
|
138
|
+
const credentials: Readonly<Record<string, ServiceCredentials>> = {
|
|
139
|
+
"store.sql": { user: "oke", password: "fixed-pw-sql", database: "oke" },
|
|
140
|
+
"store.kv": { user: "oke", password: "fixed-pw-kv", database: "oke" },
|
|
141
|
+
"store.files": { user: "oke", password: "fixed-pw-files", database: "oke" },
|
|
142
|
+
"store.index": { user: "oke", password: "fixed-pw-index", database: "oke" },
|
|
143
|
+
"channel.email": { user: "oke", password: "fixed-pw-email", database: "oke" },
|
|
144
|
+
pgdog: { user: "oke", password: "fixed-pw-pgdog", database: "oke" },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const fromFlat = deriveInfrastructure({
|
|
148
|
+
images: flatImages,
|
|
149
|
+
app: "images-nesting-parity",
|
|
150
|
+
credentials,
|
|
151
|
+
});
|
|
152
|
+
const fromNested = deriveInfrastructure({
|
|
153
|
+
images: flattenImagesConfig(nestedImages),
|
|
154
|
+
app: "images-nesting-parity",
|
|
155
|
+
credentials,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(fromNested.specs).toEqual(fromFlat.specs);
|
|
159
|
+
expect(fromNested.files).toEqual(fromFlat.files);
|
|
160
|
+
expect(fromNested.stackEnv).toEqual(fromFlat.stackEnv);
|
|
161
|
+
expect(fromNested.composeFiles).toEqual(fromFlat.composeFiles);
|
|
162
|
+
});
|
|
163
|
+
});
|
package/src/docker/index.ts
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Recipes translate a normalised {@link ServiceSpec} into image-specific
|
|
5
5
|
* env / command / healthcheck and expose {@link ImageRecipe.url} so the
|
|
6
|
-
* kernel never learns an env-var name.
|
|
7
|
-
*
|
|
6
|
+
* kernel never learns an env-var name. Default layout is a single
|
|
7
|
+
* production-grade `docker-compose.yml` (opt-in `--split` / `--stack`).
|
|
8
|
+
* Credentials never enter YAML.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export type {
|
|
11
12
|
ComposeHealthcheck,
|
|
13
|
+
ComposeLayout,
|
|
12
14
|
DeriveOptions,
|
|
13
15
|
DeriveResult,
|
|
14
16
|
GeneratedFile,
|
|
@@ -26,7 +28,11 @@ export { DEFAULT_DOCKER_DIR } from "./types.ts";
|
|
|
26
28
|
|
|
27
29
|
export {
|
|
28
30
|
COMPOSE_ALL,
|
|
31
|
+
COMPOSE_BASE,
|
|
29
32
|
COMPOSE_OVERRIDE,
|
|
33
|
+
DOCKER_COMPOSE,
|
|
34
|
+
DOCKER_COMPOSE_OVERRIDE,
|
|
35
|
+
DOCKER_STACK,
|
|
30
36
|
assertNoCredentialsInYaml,
|
|
31
37
|
buildSpecs,
|
|
32
38
|
buildStackEnv,
|
|
@@ -36,9 +42,24 @@ export {
|
|
|
36
42
|
formatStackEnv,
|
|
37
43
|
} from "./compose.ts";
|
|
38
44
|
|
|
45
|
+
export {
|
|
46
|
+
DEFAULT_SERVER_BUDGET,
|
|
47
|
+
allocateServiceResources,
|
|
48
|
+
mergeDeployResources,
|
|
49
|
+
resolveServerBudget,
|
|
50
|
+
type ServerBudget,
|
|
51
|
+
type ServiceResourceLimit,
|
|
52
|
+
} from "./resources.ts";
|
|
53
|
+
|
|
39
54
|
export { deriveInfrastructure, writeDerivedFiles } from "./derive.ts";
|
|
40
55
|
export { emitDockerfile } from "./dockerfile.ts";
|
|
41
|
-
export {
|
|
56
|
+
export {
|
|
57
|
+
composeToYaml,
|
|
58
|
+
credEnv,
|
|
59
|
+
envPrefix,
|
|
60
|
+
serviceNameFor,
|
|
61
|
+
type ComposeYamlOptions,
|
|
62
|
+
} from "./helpers.ts";
|
|
42
63
|
export { generateCredentials } from "./credentials.ts";
|
|
43
64
|
export {
|
|
44
65
|
ensureOllamaModel,
|
|
@@ -92,7 +113,6 @@ export {
|
|
|
92
113
|
ollama,
|
|
93
114
|
OLLAMA_IMAGE,
|
|
94
115
|
OLLAMA_MIN_SAFE_VERSION,
|
|
95
|
-
openbao,
|
|
96
116
|
pgdog,
|
|
97
117
|
postgres,
|
|
98
118
|
recipeFor,
|
|
@@ -108,7 +128,12 @@ export {
|
|
|
108
128
|
yugabyte,
|
|
109
129
|
} from "./recipes/index.ts";
|
|
110
130
|
export { CADDY_APP_SERVICE, buildCaddyfile } from "./recipes/caddy.ts";
|
|
111
|
-
export {
|
|
131
|
+
export {
|
|
132
|
+
PGDOG_BACKEND_SERVICE,
|
|
133
|
+
PGDOG_CONFIG_DIR,
|
|
134
|
+
buildPgDogToml,
|
|
135
|
+
buildPgDogUsersToml,
|
|
136
|
+
} from "./recipes/pgdog.ts";
|
|
112
137
|
export { SOCKET_PROXY_IMAGE, SOCKET_PROXY_SERVICE, traefikAppLabels } from "./recipes/traefik.ts";
|
|
113
138
|
export {
|
|
114
139
|
formatStackPreview,
|
|
@@ -10,7 +10,6 @@ import { llamaCpp } from "./llama-cpp.ts";
|
|
|
10
10
|
import { mailpit } from "./mailpit.ts";
|
|
11
11
|
import { meilisearch } from "./meilisearch.ts";
|
|
12
12
|
import { ollama } from "./ollama.ts";
|
|
13
|
-
import { openbao } from "./openbao.ts";
|
|
14
13
|
import { pgdog } from "./pgdog.ts";
|
|
15
14
|
import { postgres } from "./postgres.ts";
|
|
16
15
|
import { redis } from "./redis.ts";
|
|
@@ -36,7 +35,6 @@ export const builtinRecipes: readonly ImageRecipe[] = [
|
|
|
36
35
|
redis,
|
|
37
36
|
mailpit,
|
|
38
37
|
rustfs,
|
|
39
|
-
openbao,
|
|
40
38
|
meilisearch,
|
|
41
39
|
llamaCpp,
|
|
42
40
|
vllm,
|
|
@@ -54,7 +52,6 @@ export {
|
|
|
54
52
|
mailpit,
|
|
55
53
|
meilisearch,
|
|
56
54
|
ollama,
|
|
57
|
-
openbao,
|
|
58
55
|
pgdog,
|
|
59
56
|
postgres,
|
|
60
57
|
redis,
|
|
@@ -55,7 +55,7 @@ def cache_dir() -> str:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def parse_model(model: str) -> tuple[str, str]:
|
|
58
|
-
raw = model.strip() or "
|
|
58
|
+
raw = model.strip() or "granite3.3:2b"
|
|
59
59
|
if "/" not in raw.split(":", 1)[0]:
|
|
60
60
|
raw = f"ai/{raw}"
|
|
61
61
|
if ":" in raw:
|
|
@@ -187,7 +187,7 @@ def hub_pull(repo: str, tag: str, dest: str) -> None:
|
|
|
187
187
|
def ensure_model(model: str) -> str:
|
|
188
188
|
repo, tag = parse_model(model)
|
|
189
189
|
# llama download -dr expects id without forced ai/ prefix when using default org.
|
|
190
|
-
dr = model.strip() or "
|
|
190
|
+
dr = model.strip() or "granite3.3:2b"
|
|
191
191
|
if dr.startswith("ai/"):
|
|
192
192
|
dr = dr[3:]
|
|
193
193
|
dest = os.path.join(cache_dir(), expected_gguf(repo, tag))
|
|
@@ -219,7 +219,7 @@ def ensure_model(model: str) -> str:
|
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
def main() -> None:
|
|
222
|
-
model = (os.environ.get("OKE_AI_MODEL") or "
|
|
222
|
+
model = (os.environ.get("OKE_AI_MODEL") or "granite3.3:2b").strip() or "granite3.3:2b"
|
|
223
223
|
gguf = ensure_model(model)
|
|
224
224
|
# Without an explicit --ctx-size, llama-server defaults to the model's
|
|
225
225
|
# full native training context (32K-256K+ for many current models). The
|
|
@@ -259,7 +259,7 @@ export const llamaCpp: ImageRecipe = {
|
|
|
259
259
|
match: (i) => /llama\.cpp|llamacpp/i.test(i),
|
|
260
260
|
apply: (s) => ({
|
|
261
261
|
// OKE_AI_MODEL comes from compose `env_file` (`.env.docker`). Do not
|
|
262
|
-
// re-declare it as `${OKE_AI_MODEL:-
|
|
262
|
+
// re-declare it as `${OKE_AI_MODEL:-granite3.3:2b}` here — Compose interpolates
|
|
263
263
|
// that from the *host* shell and can override the stack file with the
|
|
264
264
|
// default. Never set LLAMA_ARG_MODELS_PRESET / LLAMA_ARG_DOCKER_REPO
|
|
265
265
|
// (router children inherit `LLAMA_ARG_*` and can OOM).
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sits in front of the `postgres` recipe. Transaction pooling is set
|
|
5
5
|
* explicitly (`pooler_mode = "transaction"` — also PgDog's upstream default).
|
|
6
|
-
* Config shape matches upstream docs
|
|
6
|
+
* Config shape matches upstream docs under `pgdog/`:
|
|
7
|
+
* `pgdog/pgdog.toml` + `pgdog/users.toml`
|
|
7
8
|
* (https://docs.pgdog.dev/configuration/).
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
/** Relative directory for generated PgDog TOML (under the compose dir). */
|
|
12
|
+
export const PGDOG_CONFIG_DIR = "pgdog";
|
|
13
|
+
|
|
10
14
|
import type { ImageRecipe } from "../types.ts";
|
|
11
15
|
|
|
12
16
|
/** Backend Postgres service name in the compose network. */
|
|
@@ -66,7 +70,10 @@ export const pgdog: ImageRecipe = {
|
|
|
66
70
|
port: 6432,
|
|
67
71
|
match: (i) => /pgdog/i.test(i),
|
|
68
72
|
apply: () => ({
|
|
69
|
-
volumes: [
|
|
73
|
+
volumes: [
|
|
74
|
+
`./${PGDOG_CONFIG_DIR}/pgdog.toml:/pgdog/pgdog.toml:ro`,
|
|
75
|
+
`./${PGDOG_CONFIG_DIR}/users.toml:/pgdog/users.toml:ro`,
|
|
76
|
+
],
|
|
70
77
|
dependsOn: {
|
|
71
78
|
[PGDOG_BACKEND_SERVICE]: { condition: "service_healthy" },
|
|
72
79
|
},
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server budget → per-service resource allocation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SERVER_BUDGET,
|
|
8
|
+
allocateServiceResources,
|
|
9
|
+
mergeDeployResources,
|
|
10
|
+
resolveServerBudget,
|
|
11
|
+
} from "./resources.ts";
|
|
12
|
+
|
|
13
|
+
describe("resolveServerBudget", () => {
|
|
14
|
+
test("defaults to 4 CPU / 8 GiB", () => {
|
|
15
|
+
expect(resolveServerBudget(undefined)).toEqual(DEFAULT_SERVER_BUDGET);
|
|
16
|
+
expect(DEFAULT_SERVER_BUDGET).toEqual({ cpus: 4, memoryGb: 8 });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("accepts client overrides", () => {
|
|
20
|
+
expect(resolveServerBudget({ cpus: 8, memoryGb: 16 })).toEqual({
|
|
21
|
+
cpus: 8,
|
|
22
|
+
memoryGb: 16,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("allocateServiceResources", () => {
|
|
28
|
+
test("apportion 4/8 across app + sql + kv without exceeding the usable pool", () => {
|
|
29
|
+
const limits = allocateServiceResources(["app", "store-sql", "store-kv"], {
|
|
30
|
+
cpus: 4,
|
|
31
|
+
memoryGb: 8,
|
|
32
|
+
});
|
|
33
|
+
expect(limits.size).toBe(3);
|
|
34
|
+
let cpuSum = 0;
|
|
35
|
+
let memSum = 0;
|
|
36
|
+
for (const limit of limits.values()) {
|
|
37
|
+
cpuSum += Number(limit.cpus);
|
|
38
|
+
memSum += limit.memory.endsWith("G")
|
|
39
|
+
? Number(limit.memory.slice(0, -1)) * 1024
|
|
40
|
+
: Number(limit.memory.slice(0, -1));
|
|
41
|
+
}
|
|
42
|
+
// 10% host overhead → usable 3.6 CPU / ~7372 MiB
|
|
43
|
+
expect(cpuSum).toBeLessThanOrEqual(3.6 + 0.05);
|
|
44
|
+
expect(memSum).toBeLessThanOrEqual(8 * 1024 * 0.9 + 8);
|
|
45
|
+
expect(Number(limits.get("store-sql")!.cpus)).toBeGreaterThan(
|
|
46
|
+
Number(limits.get("store-kv")!.cpus),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("mergeDeployResources preserves GPU reservations", () => {
|
|
51
|
+
const merged = mergeDeployResources(
|
|
52
|
+
{ cpus: "1.5", memory: "2G" },
|
|
53
|
+
{
|
|
54
|
+
resources: {
|
|
55
|
+
reservations: {
|
|
56
|
+
devices: [{ driver: "nvidia", count: 1, capabilities: ["gpu"] }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
const resources = merged.resources as {
|
|
62
|
+
limits: { cpus: string; memory: string };
|
|
63
|
+
reservations: { devices: unknown[]; cpus: string; memory: string };
|
|
64
|
+
};
|
|
65
|
+
expect(resources.limits).toEqual({ cpus: "1.5", memory: "2G" });
|
|
66
|
+
expect(resources.reservations.devices).toEqual([
|
|
67
|
+
{ driver: "nvidia", count: 1, capabilities: ["gpu"] },
|
|
68
|
+
]);
|
|
69
|
+
expect(resources.reservations.cpus).toBe("0.75");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server resource budget → per-service Compose `deploy.resources` limits.
|
|
3
|
+
*
|
|
4
|
+
* Defaults assume a small production host (4 CPU / 8 GiB). Callers may override
|
|
5
|
+
* via {@link ServerBudget}. ~10% stays unallocated for the Docker daemon / OS.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Host capacity used when apportioning container limits. */
|
|
9
|
+
export type ServerBudget = {
|
|
10
|
+
/** Logical CPUs available to the stack (default 4). */
|
|
11
|
+
readonly cpus: number;
|
|
12
|
+
/** RAM in GiB available to the stack (default 8). */
|
|
13
|
+
readonly memoryGb: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Default production host size when the client does not specify one. */
|
|
17
|
+
export const DEFAULT_SERVER_BUDGET: ServerBudget = {
|
|
18
|
+
cpus: 4,
|
|
19
|
+
memoryGb: 8,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Fraction of the host reserved for Docker / OS (not given to services). */
|
|
23
|
+
const HOST_OVERHEAD = 0.1;
|
|
24
|
+
|
|
25
|
+
/** Relative weights by compose role — heavier roles get a larger share. */
|
|
26
|
+
const ROLE_WEIGHT: Readonly<Record<string, number>> = {
|
|
27
|
+
app: 20,
|
|
28
|
+
"store.sql": 30,
|
|
29
|
+
pgdog: 5,
|
|
30
|
+
"store.kv": 15,
|
|
31
|
+
"store.files": 10,
|
|
32
|
+
"store.index": 10,
|
|
33
|
+
"channel.email": 5,
|
|
34
|
+
ai: 40,
|
|
35
|
+
proxy: 5,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const DEFAULT_WEIGHT = 10;
|
|
39
|
+
const MIN_CPUS = 0.1;
|
|
40
|
+
const MIN_MEMORY_MB = 64;
|
|
41
|
+
|
|
42
|
+
/** Resolved CPU + memory limit for one compose service. */
|
|
43
|
+
export type ServiceResourceLimit = {
|
|
44
|
+
readonly cpus: string;
|
|
45
|
+
readonly memory: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalise a partial budget against {@link DEFAULT_SERVER_BUDGET}.
|
|
50
|
+
*
|
|
51
|
+
* @param budget - Optional caller overrides
|
|
52
|
+
*/
|
|
53
|
+
export function resolveServerBudget(budget: Partial<ServerBudget> | undefined): ServerBudget {
|
|
54
|
+
const cpus = budget?.cpus ?? DEFAULT_SERVER_BUDGET.cpus;
|
|
55
|
+
const memoryGb = budget?.memoryGb ?? DEFAULT_SERVER_BUDGET.memoryGb;
|
|
56
|
+
if (!(cpus > 0) || !Number.isFinite(cpus)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`oke docker: server cpus must be a positive number (got ${String(budget?.cpus)})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!(memoryGb > 0) || !Number.isFinite(memoryGb)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`oke docker: server memoryGb must be a positive number (got ${String(budget?.memoryGb)})`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return { cpus, memoryGb };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apportion a server budget across named services (compose service names).
|
|
71
|
+
*
|
|
72
|
+
* @param serviceNames - Services that receive limits (`app`, `store-sql`, …)
|
|
73
|
+
* @param budget - Host capacity
|
|
74
|
+
* @returns Map of service name → deploy.resources.limits fields
|
|
75
|
+
*/
|
|
76
|
+
export function allocateServiceResources(
|
|
77
|
+
serviceNames: readonly string[],
|
|
78
|
+
budget: Partial<ServerBudget> | undefined = undefined,
|
|
79
|
+
): ReadonlyMap<string, ServiceResourceLimit> {
|
|
80
|
+
const resolved = resolveServerBudget(budget);
|
|
81
|
+
const usableCpus = resolved.cpus * (1 - HOST_OVERHEAD);
|
|
82
|
+
const usableMemoryMb = resolved.memoryGb * 1024 * (1 - HOST_OVERHEAD);
|
|
83
|
+
|
|
84
|
+
const weights = serviceNames.map((name) => {
|
|
85
|
+
const role = serviceNameToRole(name);
|
|
86
|
+
return ROLE_WEIGHT[role] ?? DEFAULT_WEIGHT;
|
|
87
|
+
});
|
|
88
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
89
|
+
const out = new Map<string, ServiceResourceLimit>();
|
|
90
|
+
if (total <= 0 || serviceNames.length === 0) return out;
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < serviceNames.length; i++) {
|
|
93
|
+
const name = serviceNames[i]!;
|
|
94
|
+
const share = weights[i]! / total;
|
|
95
|
+
const cpus = Math.max(MIN_CPUS, roundCpus(usableCpus * share));
|
|
96
|
+
const memoryMb = Math.max(MIN_MEMORY_MB, Math.round(usableMemoryMb * share));
|
|
97
|
+
out.set(name, {
|
|
98
|
+
cpus: formatCpus(cpus),
|
|
99
|
+
memory: formatMemoryMb(memoryMb),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a Compose `deploy.resources` block from a limit, preserving any
|
|
107
|
+
* existing reservations (e.g. GPU devices from AI recipes).
|
|
108
|
+
*
|
|
109
|
+
* @param limit - Allocated CPU / memory
|
|
110
|
+
* @param existing - Prior `deploy` object from a recipe, if any
|
|
111
|
+
*/
|
|
112
|
+
export function mergeDeployResources(
|
|
113
|
+
limit: ServiceResourceLimit,
|
|
114
|
+
existing: Readonly<Record<string, unknown>> | undefined,
|
|
115
|
+
): Record<string, unknown> {
|
|
116
|
+
const prev = existing ? { ...existing } : {};
|
|
117
|
+
const prevResources = isPlainObject(prev.resources)
|
|
118
|
+
? { ...prev.resources }
|
|
119
|
+
: ({} as Record<string, unknown>);
|
|
120
|
+
const prevLimits = isPlainObject(prevResources.limits)
|
|
121
|
+
? { ...prevResources.limits }
|
|
122
|
+
: ({} as Record<string, unknown>);
|
|
123
|
+
prevResources.limits = {
|
|
124
|
+
...prevLimits,
|
|
125
|
+
cpus: limit.cpus,
|
|
126
|
+
memory: limit.memory,
|
|
127
|
+
};
|
|
128
|
+
// Soft reservation ≈ half the limit so the scheduler can pack peers.
|
|
129
|
+
const prevReservations = isPlainObject(prevResources.reservations)
|
|
130
|
+
? { ...prevResources.reservations }
|
|
131
|
+
: ({} as Record<string, unknown>);
|
|
132
|
+
if (prevReservations.cpus === undefined) {
|
|
133
|
+
prevReservations.cpus = formatCpus(Math.max(MIN_CPUS, Number(limit.cpus) * 0.5));
|
|
134
|
+
}
|
|
135
|
+
if (prevReservations.memory === undefined) {
|
|
136
|
+
prevReservations.memory = formatMemoryMb(
|
|
137
|
+
Math.max(MIN_MEMORY_MB, Math.round(parseMemoryMb(limit.memory) * 0.5)),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
prevResources.reservations = prevReservations;
|
|
141
|
+
prev.resources = prevResources;
|
|
142
|
+
return prev;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function serviceNameToRole(serviceName: string): string {
|
|
146
|
+
if (serviceName === "app") return "app";
|
|
147
|
+
return serviceName.replaceAll("-", ".");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function roundCpus(n: number): number {
|
|
151
|
+
return Math.round(n * 100) / 100;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatCpus(n: number): string {
|
|
155
|
+
return String(Math.round(n * 100) / 100);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatMemoryMb(mb: number): string {
|
|
159
|
+
if (mb >= 1024 && mb % 1024 === 0) return `${mb / 1024}G`;
|
|
160
|
+
if (mb >= 1024) {
|
|
161
|
+
const gb = Math.round((mb / 1024) * 100) / 100;
|
|
162
|
+
return `${gb}G`;
|
|
163
|
+
}
|
|
164
|
+
return `${mb}M`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseMemoryMb(value: string): number {
|
|
168
|
+
const m = /^(\d+(?:\.\d+)?)(G|M)$/i.exec(value);
|
|
169
|
+
if (!m) return MIN_MEMORY_MB;
|
|
170
|
+
const n = Number(m[1]);
|
|
171
|
+
return m[2]!.toUpperCase() === "G" ? n * 1024 : n;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
175
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
176
|
+
}
|
|
@@ -63,11 +63,18 @@ describe("loadExistingStackControls", () => {
|
|
|
63
63
|
expect(controls?.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test("seeds commented OKE_AI_MODEL from .env.local when .env.docker omits it", async () => {
|
|
67
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
|
|
68
|
+
await writeFile(join(dir, ".env.local"), "# OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
|
|
69
|
+
const controls = await loadExistingStackControls(dir);
|
|
70
|
+
expect(controls?.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
|
|
71
|
+
});
|
|
72
|
+
|
|
66
73
|
test("prefers active OKE_AI_MODEL in docker/.env.docker over .env.local", async () => {
|
|
67
74
|
const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
|
|
68
75
|
await mkdir(join(dir, "docker"), { recursive: true });
|
|
69
76
|
await writeFile(join(dir, "docker", ".env.docker"), "OKE_AI_MODEL=smollm2\n", "utf8");
|
|
70
|
-
await writeFile(join(dir, ".env.local"), "OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
|
|
77
|
+
await writeFile(join(dir, ".env.local"), "# OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
|
|
71
78
|
const controls = await loadExistingStackControls(dir);
|
|
72
79
|
expect(controls?.OKE_AI_MODEL).toBe("smollm2");
|
|
73
80
|
});
|
|
@@ -132,7 +139,7 @@ describe("deriveInfrastructure instanceId", () => {
|
|
|
132
139
|
},
|
|
133
140
|
},
|
|
134
141
|
});
|
|
135
|
-
const base = result.files.find((f) => f.path === "compose.yml")!.content;
|
|
142
|
+
const base = result.files.find((f) => f.path === "docker-compose.yml")!.content;
|
|
136
143
|
expect(base).toContain("oke-dev-abcdef");
|
|
137
144
|
expect(result.stackEnv.DATABASE_URL).toMatch(/:15\d{3}\//);
|
|
138
145
|
expect(result.stackEnv.REDIS_URL).toMatch(/:16\d{3}/);
|
|
@@ -163,8 +170,8 @@ describe("deriveInfrastructure instanceId", () => {
|
|
|
163
170
|
},
|
|
164
171
|
},
|
|
165
172
|
});
|
|
166
|
-
const mailYml = result.files.find((f) => f.path === "compose.
|
|
167
|
-
const filesYml =
|
|
173
|
+
const mailYml = result.files.find((f) => f.path === "docker-compose.yml")!.content;
|
|
174
|
+
const filesYml = mailYml;
|
|
168
175
|
expect(mailYml).toContain(`${20_000 + n}:1025`);
|
|
169
176
|
expect(mailYml).toContain(`${21_000 + n}:8025`);
|
|
170
177
|
expect(mailYml).not.toContain("8025:8025");
|