diff --git a/.changeset/sv-ui-flags.md b/.changeset/sv-ui-flags.md new file mode 100644 index 0000000..ca6295f --- /dev/null +++ b/.changeset/sv-ui-flags.md @@ -0,0 +1,5 @@ +--- +"@bootnodedev/cbn": minor +--- + +Add per-UI `sv` config flags (`scanUI`, `svUI`, `walletUI`) to toggle the SV-facing web UIs. A static override shipped with the package (`templates/runtime-overrides.yaml`) pins each disabled UI to 0 replicas and aliases its hostname onto nginx — driven purely by env vars — so the stack stays healthy and the SV's API routes keep working either way. diff --git a/README.md b/README.md index b7f765f..38f5cff 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Run `npm run init` once to scaffold `canton-barebones.config.json`, then edit it A Canton network here has three kinds of pieces: -- **SV** (Super Validator) — the node that runs the _global synchronizer_, the shared backbone all participants connect to. It is required infrastructure, so it always runs fully (backend + its web dashboards). It has nothing to configure. +- **SV** (Super Validator) — the node that runs the _global synchronizer_, the shared backbone all participants connect to. It is required infrastructure, so its backend always runs; only its web dashboards are configurable (the `sv` flags). - **Validators** — the participant nodes that run your apps. Splice's LocalNet ships two fixed slots, `appProvider` and `appUser`. Each has `enabled` (run its backend) and `ui` (also expose its web UIs). - **Network tools** — utilities that work across the nodes: the Canton `console`, `multiSync` (adds a second, local synchronizer), and `swaggerUI` (API docs). @@ -91,6 +91,8 @@ A Canton network here has three kinds of pieces: "appUser": { "enabled": true, "ui": false }, // default: backend on, UIs off (headless) }, + "sv": { "scanUI": true, "svUI": true, "walletUI": true }, // the SV's web UIs, toggleable per UI + "networkTools": { "console": false, "multiSync": false, "swaggerUI": false }, } ``` @@ -99,6 +101,7 @@ Rules: - `ui` needs the backend, so it can only be `true` when that validator's `enabled` is `true`. - A validator's UIs come as one bundle (wallet + ANS): it is on-or-off per validator, not per individual UI. `enabled: true, ui: false` runs it **headless** — backend only, reached on its direct API ports. +- The SV backend is not configurable — it is required infrastructure and always runs. Its web UIs are, individually: an `sv` flag off skips that UI container, while the API routes on the SV's nginx port keep working (a disabled UI's URL answers 502). - Config changes take effect on the next `start`. ### How your config becomes running services @@ -110,25 +113,28 @@ Rules: | always | `--profile sv` runs the SV fully and, with it, the shared postgres/canton/splice/nginx | | `validators.*.enabled` | switches that validator's backend on/off via an env var | | `validators.*.ui` | on → also starts that validator's UIs; off (but enabled) → nginx is told to skip that validator so it runs headless | +| an `sv` UI flag off | env vars pin that UI to 0 replicas and alias its hostname onto nginx (static `templates/runtime-overrides.yaml`) | | a `networkTools` flag on | starts that tool via its profile | So the default config launches the SV plus a headless `appUser`, and each flag you flip adds more. (For _why_ a headless validator needs special handling, see [Design notes](#design-notes).) ## UIs and endpoints -Assuming every flag is on, this is everything the stack exposes. Each participant gets its own nginx port (`sv` 4000, `app-provider` 3000, `app-user` 2000), and its web UIs and ledger APIs are reached through hostnames on that port. A validator's routes only appear when its `ui: true`; the SV is always on. All `*.localhost` names resolve to `127.0.0.1` automatically. +Assuming every flag is on, this is everything the stack exposes. Each participant gets its own nginx port (`sv` 4000, `app-provider` 3000, `app-user` 2000), and its web UIs and ledger APIs are reached through hostnames on that port. A validator's routes only appear when its `ui: true`; the SV's APIs are always on and its web UIs follow the `sv` flags. All `*.localhost` names resolve to `127.0.0.1` automatically. > In **headless** mode (`enabled: true, ui: false`) a validator's nginx routes below are not exposed. Reach its ledger API on the direct participant ports instead: JSON on `975`, gRPC on `901`, where the prefix is `2`/`3`/`4` — e.g. `localhost:2975` (JSON) and `localhost:2901` (gRPC) for app-user. -### SV (port 4000, always on) +### SV (port 4000, backend always on) + +The three web UIs each have an `sv` config flag; turning one off makes its URL answer 502 while the API rows keep working. -| Surface | URL | -| ----------------------- | ----------------------------------------- | -| SV operations dashboard | http://sv.localhost:4000 | -| Scan (network explorer) | http://scan.localhost:4000 | -| Wallet | http://wallet.localhost:4000 | -| JSON Ledger API | http://canton.localhost:4000/v2 | -| OpenAPI spec | http://canton.localhost:4000/docs/openapi | +| Surface | URL | Requires | +| ----------------------- | ----------------------------------------- | ------------- | +| SV operations dashboard | http://sv.localhost:4000 | `sv.svUI` | +| Scan (network explorer) | http://scan.localhost:4000 | `sv.scanUI` | +| Wallet | http://wallet.localhost:4000 | `sv.walletUI` | +| JSON Ledger API | http://canton.localhost:4000/v2 | always | +| OpenAPI spec | http://canton.localhost:4000/docs/openapi | always | ### app-user (port 2000, requires `validators.appUser.ui: true`) @@ -176,11 +182,11 @@ The binary is `canton-barebones `; the `npm run ` scripts wrap **Exit codes & output.** Every command exits `0` on success and `1` on failure, printing the error message to stderr. -**`--json`** (on `validate`, `setup`, `status`) switches to machine-readable output: success goes to stdout, and on failure a `{ "ok": false, "error": "…" }` object goes to stderr, still exiting `1`. Use `validate --json` to see exactly what a config resolves to **without starting anything** — its `plan` field lists the compose profiles, headless validators, and participant env: +**`--json`** (on `validate`, `setup`, `status`) switches to machine-readable output: success goes to stdout, and on failure a `{ "ok": false, "error": "…" }` object goes to stderr, still exiting `1`. Use `validate --json` to see exactly what a config resolves to **without starting anything** — its `plan` field lists the compose profiles, headless validators, disabled SV UIs, and participant env: ```bash canton-barebones validate --json -# { "ok": true, "plan": { "upProfiles": ["sv","app-user"], "headlessValidators": [], "nodeEnv": {…} }, … } +# { "ok": true, "plan": { "upProfiles": ["sv","app-user"], "headlessValidators": [], "disabledSvUIs": [], "nodeEnv": {…} }, … } canton-barebones status --json # one JSON object per service, straight from docker compose ``` @@ -281,3 +287,10 @@ nginx renders an empty app-user.conf → no routes for it → starts fine. ``` The validator's backend still runs (it is driven by an env var, not nginx) and stays reachable on its direct API ports; only its web routing is dropped. See `templates/splice-localnet-overrides.yaml` and `src/compose.js` for the full detail. + +### How a disabled SV web UI works (replicas + alias) + +Turning off an SV web UI (`sv.scanUI` / `sv.svUI` / `sv.walletUI`) cannot reuse the empty-template trick above: Splice's `sv.conf` mixes the UI routes with **API routes** (scan API, SV admin API, canton JSON API) that proxy to the always-running `splice`/`canton` containers and must stay up, and the flags are per-UI rather than all-or-nothing. Instead, a **static** override shipped inside the package (`templates/runtime-overrides.yaml`, always applied) uses two other compose levers, driven purely by env vars that the wrapper writes to `.generated/localnet.env`: + +1. `deploy.replicas: ${…_REPLICAS}` — `0` for a disabled UI, so compose never starts its container. (An override entry rather than `docker compose --scale`, which errors for services whose profile is not selected.) +2. nginx still resolves that UI's hostname at startup (`proxy_pass http://scan-web-ui:8080/` …) and would crash with "host not found in upstream" once no container owns the name. So nginx carries a **network alias** per UI whose _value_ comes from `${…_NGINX_ALIAS}`: for a disabled UI it is the real hostname — the name resolves onto the nginx container itself, nginx boots, and since nothing there listens on the UI port, browsing the disabled UI answers **502** while every API route on port 4000 keeps working. For an enabled UI it is an inert `-unused` (static YAML has no conditionals, so the list entry always exists and only its value changes). diff --git a/scripts/config-validation.test.js b/scripts/config-validation.test.js index 0346bb8..d76d68a 100644 --- a/scripts/config-validation.test.js +++ b/scripts/config-validation.test.js @@ -24,10 +24,10 @@ function assertRejects(raw, expectedFragment) { // The scaffolded default shipped by `init`: version 1, a pinned Splice source, // persistent volumes, app-provider off, app-user enabled headless (backend on, -// UIs off), and all network tools off. The SV is not in the config — it is -// required infrastructure that always runs fully. This is the baseline; every -// negative case below clones it and breaks a single rule, so a failure points to -// one validation concern. +// UIs off), every SV web UI on (the SV backend itself is not in the config — it +// is required infrastructure that always runs), and all network tools off. This +// is the baseline; every negative case below clones it and breaks a single rule, +// so a failure points to one validation concern. const validConfig = { version: 1, splice: { repo: 'canton-network/splice', tag: '0.6.11' }, @@ -38,6 +38,7 @@ const validConfig = { appProvider: { enabled: false, ui: false }, appUser: { enabled: true, ui: false }, }, + sv: { scanUI: true, svUI: true, walletUI: true }, networkTools: { console: false, multiSync: false, swaggerUI: false }, }; @@ -89,11 +90,12 @@ describe('version gating', () => { // Scenario: strictness. Unknown fields must fail loudly instead of being ignored, // so typos and stale keys surface immediately. describe('unknown fields', () => { - // A stale top-level key (e.g. the removed `sv`) must be rejected. + // A stale top-level key (e.g. a leftover from an older config shape) must be + // rejected. it('rejects an unknown top-level field', () => { const raw = clone(validConfig); - raw.sv = { modules: {} }; - assertRejects(raw, /sv/); + raw.globalModules = {}; + assertRejects(raw, /globalModules/); }); // Strictness must extend into nested objects: the removed per-validator @@ -152,6 +154,49 @@ describe('required and typed fields', () => { }); }); +// Scenario: the SV web UI flags. The SV backend always runs, so the `sv` section +// only carries its three per-UI flags; the section is required and each flag must +// be an explicit boolean. +describe('sv web UI flags', () => { + // Any combination of the three flags is valid — here scan on, the rest off — + // because each UI is independent (unlike a validator's all-or-nothing bundle). + it('accepts a partial set of SV UIs turned off', () => { + const raw = clone(validConfig); + raw.sv = { scanUI: true, svUI: false, walletUI: false }; + assert.doesNotThrow(() => parseConfig(raw)); + }); + + // `sv` is a required section: the flags default to nothing, so a config + // without them must fail rather than silently picking a behavior. + it('rejects a missing sv section', () => { + const raw = clone(validConfig); + delete raw.sv; + assertRejects(raw, /sv/); + }); + + // All three flags must be present — a missing one is a stale or typoed config. + it('rejects a missing sv UI flag', () => { + const raw = clone(validConfig); + delete raw.sv.walletUI; + assertRejects(raw, /walletUI/); + }); + + // Strictness inside the section: an unknown flag (e.g. an ANS UI, which only + // validators have) must be rejected. + it('rejects an unknown sv UI flag', () => { + const raw = clone(validConfig); + raw.sv.ansUI = true; + assertRejects(raw, /ansUI/); + }); + + // Flags must be explicit booleans; a string "false" is a common mistake. + it('rejects a non-boolean sv UI flag', () => { + const raw = clone(validConfig); + raw.sv.scanUI = 'false'; + assertRejects(raw, /scanUI/); + }); +}); + // Scenario: validator enabled/ui consistency. A validator's UIs are reached // through its backend, so `ui` cannot be on while the validator is disabled. describe('validator enabled/ui consistency', () => { diff --git a/scripts/runtime-plan.test.js b/scripts/runtime-plan.test.js new file mode 100644 index 0000000..a96ea7f --- /dev/null +++ b/scripts/runtime-plan.test.js @@ -0,0 +1,106 @@ +import { describe, it, after } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { deriveRuntimePlan, writeLocalnetEnv } from '../src/compose.js'; + +// A minimal config slice covering only what deriveRuntimePlan and +// writeLocalnetEnv read: the validator flags, the SV UI flags, the network tool +// flags, the identifiers echoed into the env file, and a directory to generate +// into. The baseline mirrors the scaffolded default — app-provider off, app-user +// headless, all SV UIs on, tools off — and each case below flips one lever. +function baseConfig(generatedDir) { + return { + imageTag: '0.6.11', + composeProjectName: 'canton-barebones', + dockerNetwork: 'cantonBarebones', + localnetDir: '/tmp/localnet', + localnetEnvDir: '/tmp/localnet/env', + validators: { + appProvider: { enabled: false, ui: false }, + appUser: { enabled: true, ui: false }, + }, + sv: { scanUI: true, svUI: true, walletUI: true }, + networkTools: { console: false, multiSync: false, swaggerUI: false }, + generatedDir, + }; +} + +// A throwaway directory standing in for the project's .generated/, so the tests +// can inspect the env file the writer produces without touching the repo. +const generatedDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbn-runtime-plan-')); +after(() => fs.rmSync(generatedDir, { recursive: true, force: true })); + +// Parses a generated env file back into a key→value map so assertions can target +// individual variables instead of matching on raw file contents. +function readEnvFile(envPath) { + const entries = fs + .readFileSync(envPath, 'utf8') + .split('\n') + .filter(line => line.includes('=')) + .map(line => line.split(/=(.*)/s).slice(0, 2)); + return Object.fromEntries(entries); +} + +// Scenario: mapping the SV UI flags onto the plan. Each disabled flag must +// surface as its Docker Compose service name — that name is what the static +// runtime override targets (both the replicas pin and the nginx alias), and it +// is echoed in `validate --json` for the user. +describe('deriveRuntimePlan sv UI flags', () => { + // All flags on (the default) → nothing disabled, and the profile set stays the + // usual sv-only baseline. Guards against the sv section accidentally growing + // profile side effects: SV UIs ride the always-on `sv` profile. + it('reports no disabled SV UIs when every flag is on', () => { + const plan = deriveRuntimePlan(baseConfig(generatedDir)); + assert.deepEqual(plan.disabledSvUIs, []); + assert.deepEqual(plan.upProfiles, ['sv']); + }); + + // Turning off scanUI and walletUI while keeping svUI must list exactly the two + // matching service names — proving the flag→service mapping and that flags are + // independent (unlike a validator's all-or-nothing ui bundle). + it('maps each disabled flag to its compose service name', () => { + const config = baseConfig(generatedDir); + config.sv = { scanUI: false, svUI: true, walletUI: false }; + const plan = deriveRuntimePlan(config); + assert.deepEqual(plan.disabledSvUIs, ['scan-web-ui', 'wallet-web-ui-sv']); + }); +}); + +// Scenario: the SV UI env vars. templates/runtime-overrides.yaml is static and +// consumes one replicas + alias pair per UI, so these vars ARE the runtime +// contract: replicas 0/1 decides whether the container starts, and the alias +// keeps a disabled UI's hostname resolvable (nginx dies at startup on an +// unresolvable upstream — Splice's sv.conf proxies to these hostnames +// unconditionally). An enabled UI gets an inert "-unused" alias because the +// static YAML list entry always exists and only its value can change. +describe('writeLocalnetEnv sv UI vars', () => { + // All UIs on (the default): every service keeps 1 replica and nginx only + // holds inert aliases, leaving the real hostnames to the UI containers. + it('writes 1 replica and an inert alias for enabled UIs', () => { + const envPath = writeLocalnetEnv(baseConfig(generatedDir)); + const env = readEnvFile(envPath); + for (const prefix of ['SCAN_WEB_UI', 'SV_WEB_UI', 'WALLET_WEB_UI_SV']) { + assert.equal(env[`${prefix}_REPLICAS`], '1'); + } + assert.equal(env.SCAN_WEB_UI_NGINX_ALIAS, 'scan-web-ui-unused'); + assert.equal(env.SV_WEB_UI_NGINX_ALIAS, 'sv-web-ui-unused'); + assert.equal(env.WALLET_WEB_UI_SV_NGINX_ALIAS, 'wallet-web-ui-sv-unused'); + }); + + // scanUI off: its service must drop to 0 replicas and nginx must take over the + // real `scan-web-ui` hostname, while the other two UIs stay untouched. + it('writes 0 replicas and the real hostname alias for a disabled UI', () => { + const config = baseConfig(generatedDir); + config.sv = { scanUI: false, svUI: true, walletUI: true }; + const env = readEnvFile(writeLocalnetEnv(config)); + assert.equal(env.SCAN_WEB_UI_REPLICAS, '0'); + assert.equal(env.SCAN_WEB_UI_NGINX_ALIAS, 'scan-web-ui'); + assert.equal(env.SV_WEB_UI_REPLICAS, '1'); + assert.equal(env.SV_WEB_UI_NGINX_ALIAS, 'sv-web-ui-unused'); + assert.equal(env.WALLET_WEB_UI_SV_REPLICAS, '1'); + assert.equal(env.WALLET_WEB_UI_SV_NGINX_ALIAS, 'wallet-web-ui-sv-unused'); + }); +}); diff --git a/scripts/smoke.js b/scripts/smoke.js index 9d85aae..e98c1bf 100644 --- a/scripts/smoke.js +++ b/scripts/smoke.js @@ -20,6 +20,7 @@ assert.deepEqual(config.validators, { appProvider: { enabled: false, ui: false }, appUser: { enabled: true, ui: false }, }); +assert.deepEqual(config.sv, { scanUI: true, svUI: true, walletUI: true }); assert.deepEqual(config.networkTools, { console: false, multiSync: false, swaggerUI: false }); // With the scaffolded default (app-provider off, app-user enabled headless, tools @@ -30,6 +31,8 @@ assert.deepEqual(config.networkTools, { console: false, multiSync: false, swagge const plan = deriveRuntimePlan(config); assert.deepEqual(plan.upProfiles, ['sv']); assert.deepEqual(plan.headlessValidators, ['appUser']); +// All SV web UIs are on by default, so nothing is pinned to 0 replicas. +assert.deepEqual(plan.disabledSvUIs, []); assert.equal(runtimeEnvPath.endsWith('.generated/localnet.env'), true); assert.equal(config.localnetOverridePath.endsWith('splice-localnet-overrides.yaml'), true); assert.match(localnetOverride, /max-size: "25m"/); diff --git a/src/compose.js b/src/compose.js index 6756094..f8ca380 100644 --- a/src/compose.js +++ b/src/compose.js @@ -11,10 +11,16 @@ // - A validator with its backend on but UIs off would still make nginx try to // route to its (absent) UIs and fail. To run it headless we blank out its // nginx route with a generated override (see writeGeneratedOverride). +// - The SV's web UIs ride the always-on `sv` profile, so turning one off is not +// a profile decision either: a static override shipped with this package +// (templates/runtime-overrides.yaml) pins it to 0 replicas and keeps nginx +// bootable, driven purely by env vars written here (see writeLocalnetEnv). import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; +import { resolveFromPackage } from './paths.js'; + // Every profile this wrapper can select. Used by teardown (stop/reset) to make // sure containers from any profile are removed, not just the ones currently up. export const allLocalnetProfiles = ['app-provider', 'app-user', 'sv', 'swagger-ui', 'console', 'multi-sync']; @@ -33,6 +39,11 @@ const NGINX_ROUTE_TEMPLATE = { // Compose profile for each network tool. const TOOL_PROFILE = { console: 'console', multiSync: 'multi-sync', swaggerUI: 'swagger-ui' }; +// The SV web UI service behind each `sv.*` config flag. These live in the +// always-selected `sv` profile, so a disabled one must be kept from starting via +// the static runtime override rather than by dropping a profile. +const SV_UI_SERVICE = { scanUI: 'scan-web-ui', svUI: 'sv-web-ui', walletUI: 'wallet-web-ui-sv' }; + // Formats one Docker env-file line and strips newlines to keep generated files valid. function envLine(key, value) { return `${key}=${String(value).replaceAll(/[\r\n]/g, '')}`; @@ -52,6 +63,8 @@ function profileFlags(profiles) { // are wanted, plus a profile per enabled network tool. // - headlessValidators: validators whose backend is on but UIs are off; nginx must // be told to skip their routes (see writeGeneratedOverride). +// - disabledSvUIs: SV web UI services turned off in the config; the static +// runtime override pins them to 0 replicas via env vars (see writeLocalnetEnv). export function deriveRuntimePlan(config) { const { appProvider, appUser } = config.validators; @@ -84,7 +97,11 @@ export function deriveRuntimePlan(config) { } } - return { nodeEnv, upProfiles, headlessValidators }; + const disabledSvUIs = Object.entries(config.sv) + .filter(([, on]) => !on) + .map(([flag]) => SV_UI_SERVICE[flag]); + + return { nodeEnv, upProfiles, headlessValidators, disabledSvUIs }; } // Writes runtime env files used to bridge this repo's config into Splice LocalNet. @@ -95,6 +112,19 @@ export function writeLocalnetEnv(config) { fs.writeFileSync(emptyEnvPath, ''); const { nodeEnv } = deriveRuntimePlan(config); + + // One replicas + alias pair per SV web UI, consumed by the static + // templates/runtime-overrides.yaml (see its header for how the two work). The + // env prefix is the service name upper-snake-cased, e.g. SCAN_WEB_UI. + const svUiEnv = Object.entries(SV_UI_SERVICE).flatMap(([flag, service]) => { + const on = config.sv[flag]; + const prefix = service.replaceAll('-', '_').toUpperCase(); + return [ + envLine(`${prefix}_REPLICAS`, on ? 1 : 0), + envLine(`${prefix}_NGINX_ALIAS`, on ? `${service}-unused` : service), + ]; + }); + const contents = [ envLine('IMAGE_TAG', config.imageTag), envLine('COMPOSE_PROJECT_NAME', config.composeProjectName), @@ -105,6 +135,7 @@ export function writeLocalnetEnv(config) { envLine('SV_PROFILE', nodeEnv.SV_PROFILE), envLine('APP_PROVIDER_PROFILE', nodeEnv.APP_PROVIDER_PROFILE), envLine('APP_USER_PROFILE', nodeEnv.APP_USER_PROFILE), + ...svUiEnv, '', ].join('\n'); @@ -158,6 +189,11 @@ export function dockerComposeArgs(config, options = {}) { // constraints keep it from starving the host and match how Splice runs LocalNet. args.push('-f', path.resolve(config.localnetDir, 'resource-constraints.yaml')); + // The package's static runtime override translates the `sv` UI env vars into + // replica pins and nginx aliases; it ships with the package (not scaffolded), + // and goes before the user's override so user tweaks can still win the merge. + args.push('-f', resolveFromPackage('templates/runtime-overrides.yaml')); + args.push('-f', config.localnetOverridePath); // The generated override must come last so its empty nginx templates win the merge. diff --git a/src/config.js b/src/config.js index d125724..d774873 100644 --- a/src/config.js +++ b/src/config.js @@ -3,8 +3,9 @@ // with a clear message instead of failing deep inside Docker later, and then // resolves the extra runtime paths the rest of the tool needs. What the config // turns on and off, described inline with each schema: the two validators -// (backend and, optionally, their UIs) and the network tools. The SV is not -// configurable — it is required infrastructure and always runs fully. +// (backend and, optionally, their UIs), the SV's web UIs, and the network tools. +// The SV backend is not configurable — it is required infrastructure and always +// runs — but each of its web UIs can be switched off individually. import fs from 'node:fs'; import path from 'node:path'; import { z } from 'zod'; @@ -74,8 +75,8 @@ const validatorSchema = z // Splice LocalNet only exposes two participant validators; both are always present // in the config and toggled via `enabled`, so this is a fixed-key object rather // than an open array. (The SV is not here: it is required infrastructure — it -// founds the global synchronizer validators connect to — so it always runs fully, -// with its own UIs, and has nothing to toggle.) +// founds the global synchronizer validators connect to — so its backend always +// runs; only its web UIs are configurable, via the separate `sv` section.) const validatorsSchema = z .object({ appProvider: validatorSchema, @@ -83,6 +84,19 @@ const validatorsSchema = z }) .strict(); +// The SV's web UIs. Unlike a validator's all-or-nothing `ui` bundle, these can be +// toggled per UI: each flag only controls whether that UI container runs, while +// the APIs behind the same nginx port (scan API, SV admin API, canton JSON API) +// proxy to the always-running splice/canton containers and stay reachable either +// way. A disabled UI's URL answers 502 instead of serving the app. +const svSchema = z + .object({ + scanUI: z.boolean(), + svUI: z.boolean(), + walletUI: z.boolean(), + }) + .strict(); + // Cross-node tooling that operates over the participants (SV + validators): the // Canton console, the extra local application synchronizer, and the aggregated // Swagger UI. All are optional and off by default. @@ -105,6 +119,7 @@ const configSchema = z dockerNetwork: z.string().min(1, 'dockerNetwork must be a non-empty string'), persistence: persistenceSchema, validators: validatorsSchema, + sv: svSchema, networkTools: networkToolsSchema, }) .strict(); diff --git a/templates/canton-barebones.config.json b/templates/canton-barebones.config.json index 77bea2e..7690608 100644 --- a/templates/canton-barebones.config.json +++ b/templates/canton-barebones.config.json @@ -19,9 +19,14 @@ "ui": false } }, + "sv": { + "scanUI": false, + "svUI": false, + "walletUI": false + }, "networkTools": { "console": false, "multiSync": false, "swaggerUI": false } -} +} \ No newline at end of file diff --git a/templates/runtime-overrides.yaml b/templates/runtime-overrides.yaml new file mode 100644 index 0000000..5bebe93 --- /dev/null +++ b/templates/runtime-overrides.yaml @@ -0,0 +1,39 @@ +# Compose override shipped INSIDE the canton-barebones package — it is always +# appended to the compose -f chain and is not scaffolded into your project (put +# your own tweaks in splice-localnet-overrides.yaml instead). It turns the `sv` +# config flags into runtime behavior using only env-var substitution; the values +# come from .generated/localnet.env (see writeLocalnetEnv in src/compose.js). +# +# Two levers per SV web UI: +# +# - replicas: 1 keeps Splice's default; 0 stops a disabled UI from starting. +# (An override entry rather than `docker compose --scale`, which errors for +# services whose profile is not selected, while an override entry stays inert.) +# - alias: Splice's sv.conf proxies to each UI's hostname as a fixed upstream, +# and nginx resolves those at startup, dying with "host not found in +# upstream" if a disabled UI's name no longer exists. A disabled UI's alias +# is therefore its real hostname, pointed at the nginx container itself: the +# name resolves, nginx boots, and browsing the disabled UI answers 502 while +# every API route on the SV port keeps working. An enabled UI gets an inert +# "-unused" alias instead — static YAML has no conditionals, so the +# list entry always exists and only its value changes. +services: + nginx: + networks: + default: + aliases: + - ${SCAN_WEB_UI_NGINX_ALIAS} + - ${SV_WEB_UI_NGINX_ALIAS} + - ${WALLET_WEB_UI_SV_NGINX_ALIAS} + + scan-web-ui: + deploy: + replicas: ${SCAN_WEB_UI_REPLICAS} + + sv-web-ui: + deploy: + replicas: ${SV_WEB_UI_REPLICAS} + + wallet-web-ui-sv: + deploy: + replicas: ${WALLET_WEB_UI_SV_REPLICAS} diff --git a/templates/splice-localnet-overrides.yaml b/templates/splice-localnet-overrides.yaml index 45673f8..8d11bee 100644 --- a/templates/splice-localnet-overrides.yaml +++ b/templates/splice-localnet-overrides.yaml @@ -22,6 +22,11 @@ # still runs and stays reachable on its direct API ports. # # See src/compose.js (deriveRuntimePlan / writeGeneratedOverride) for how it is built. +# +# Finally, a THIRD, STATIC override ships inside the canton-barebones package +# (templates/runtime-overrides.yaml, always applied before this file): it turns +# the `sv` config flags into 0-replica pins and nginx aliases for the SV web UIs, +# driven purely by env vars. See that file's header for the full detail. services: postgres: logging: &default-logging