R2D2-MERIDIAN/.env.local.example
Joshua Belke 6893ecfbd5 feat(bus): land the single-host loopback arm of the zenohd profile
Phase 5.1 of `.settings/features/feature-zenoh-transport.md`, split per the
Security & Identity council's chair call on meridian-nok3. The held commit
060b755c5 carried Compose *and* Helm; this lands only the arm the relay can
actually boot against, and holds the rest.

WHAT LANDS: the root `docker-compose.yml` `bus` profile (opt-in, default off),
`deploy/compose/zenoh/zenohd.json5` as the one router config both stacks will
mount read-only, `docker-compose.override.yml.example`, `.env.example`,
`.env.local.example`, `run.sh` with `zenoh` added to `PORT_KEYS`,
`.settings/run-profiles/local.yaml`, and the `env-doctor` bus report. Every
endpoint it renders is `tcp/localhost:<port>`.

WHAT IS HELD: `deploy/charts/**`, `deploy/compose/compose.yml`,
`compose.dev.yml`, `deploy/compose/.env.example` and
`deploy/compose/README.md`. Those render `tcp/zenohd:7447` and
`tcp/<pod>.<service>:7447`, and `ZenohBusConfig::validate` refuses a reachable
endpoint while no lane floor exceeds the authenticated link profile — which
under the in-deployment floors is unconditional, and which `from_env` cannot
raise by design. Confirmed against the binary built from this tree, not only
against `a_reachable_endpoint_is_refused_while_the_link_profile_meets_every_floor`:

    MERIDIAN_BUS=zenoh MERIDIAN_ZENOH_ENDPOINTS=tcp/zenohd:7447 -> exit 1,
    "endpoint `tcp/zenohd:7447` is reachable beyond this host ... an
     event-injection primitive"

The held chart went further and made that state the documented default:
`_validate.tpl` failed the render unless endpoints were supplied whenever
`bus.mode != redis`. Shipping it would have pushed operators into a guaranteed
crash rather than merely allowing one.

Three findings from zenohd's own source shape the router config, none visible
from its documented surface. zenohd runs `config.adminspace.set_enabled(true)`
and `config.plugins_loading.set_enabled(true)` unconditionally after loading
the file, so the levers that hold are `--adminspace-permissions none`,
`plugins_loading.search_dirs: []`, an empty `plugins: {}`, and never `-P`.
Multicast scouting is force-ENABLED when the key is unset, so
`--no-multicast-scouting` is what survives a config swap, while gossip has no
CLI flag at all — which is why mounting the file is mandatory rather than a
convenience. And `transport.shared_memory.enabled` defaults TRUE in Zenoh 1.x,
so it is set explicitly false; Phase 6 is unscheduled and gated on a measured
crossover plus memlock/CAP_IPC_LOCK.

`run.sh`, `PORT_KEYS` and `local.yaml` move in this commit and not before: a
generated `zenohd:` override block against a base file with no `zenohd` service
makes `docker compose config` fail outright. The port is allocated whether or
not the profile is on, so enabling it later moves nothing.

Two fixes on top of the held commit. Its `.env.local` and override heredocs are
unquoted and its new comments carried raw backticks, so every `./run.sh`
invocation ran `bus` as a command — three `bus: command not found` lines on
stderr and the word deleted from the generated comment. Backticks are escaped
the way the surrounding lines already do it. And its `.env.example` block
re-documented `MERIDIAN_BUS` a second time, claiming `redis` is the only
shippable value and that the other modes exist "not in a deployable artifact";
that was already only half true and is now false, so the block points at the
canonical one and states the real boundary, which is loopback.

Also carried over: `env-doctor` exited 1 on any tree without a `.env`, because
the recipe exports `COMPOSE_ENV_FILES=.env` while `.env` is gitignored — the
tool you reach for BECAUSE the layers are confusing died before printing a
port. It now reports what was asked for, then drops the names that do not
exist.

Gates, each read as an exit code rather than from its output:
  just compose-check            0 with COMPOSE_PROFILES=...,bus and 0 without
                                (63 assertions, 0 failed, both ways)
  just env-doctor               0 both ways, including on a tree with no .env
  docker compose --profile bus config   0; `published: "7447"` appears once,
                                so `ports: !override` replaced rather than
                                appended
  just launcher-check           0
  ./run.sh ports --fresh-ports  0, and reproduces the saved allocation exactly
                                across three runs — every port equals its
                                profile starting point, zenoh included
  just zenoh-check              0
  just check-docker-context     0

Signed-off-by: Joshua Belke <admin@aipowergrid.io>
Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
2026-08-20 12:18:25 -04:00

84 lines
4.5 KiB
Bash

# =============================================================================
# .env.local.example — template for machine-specific overrides
# =============================================================================
# Copy to .env.local (gitignored) and edit:
# cp .env.local.example .env.local
#
# .env holds what the whole team shares. .env.local holds what is true only on
# YOUR machine: relocated ports, local URLs, which optional services you run.
# Never commit .env.local.
#
# -----------------------------------------------------------------------------
# How each file is actually loaded — this part is not obvious
# -----------------------------------------------------------------------------
# .env Compose reads it automatically. `just` reads it
# automatically (`set dotenv-load := true`).
#
# .env.local Compose reads it ONLY when COMPOSE_ENV_FILES is
# set IN THE SHELL. Putting COMPOSE_ENV_FILES
# inside .env is silently ignored — verified, and
# a tempting dead end. Use `just up` / `just
# compose ...`, which export it for you, or
# export it yourself.
#
# docker-compose.override.yml Compose auto-loads it with no flags and no env
# wiring at all. It is the ONLY mechanism that
# survives a bare `docker compose up -d`, which
# is why port pinning lives there too.
#
# Precedence, lowest to highest:
# .env < .env.local < docker-compose.override.yml < shell environment
#
# If you relocate a port, change it in BOTH .env.local and
# docker-compose.override.yml. `just compose-check` fails if they drift.
# =============================================================================
# -----------------------------------------------------------------------------
# Port relocations
# -----------------------------------------------------------------------------
# 6379/3210/6791 collide with other local stacks more often than not. A
# container from another Compose project publishing the same port can silently
# win the bind, leaving your stack half-working.
#
# REDIS_HOST_PORT (what Compose publishes) and REDIS_URL (what the relay dials)
# MUST move together. Changing only one produces `Connection refused
# (os error 61)` from the relay with everything else looking healthy.
# REDIS_HOST_PORT=6390
# REDIS_URL=redis://localhost:6390
#
# zenohd (COMPOSE_PROFILES=bus). Same rule, same failure: ZENOH_HOST_PORT is
# what Compose publishes and MERIDIAN_ZENOH_ENDPOINTS is what the relay dials.
# Mirror any relocation in docker-compose.override.yml with `ports: !override`.
# Neither multicast nor gossip scouting is enabled, so an endpoint the relay
# was not explicitly given is unreachable — there is no discovery fallback.
# ZENOH_HOST_PORT=7448
# MERIDIAN_ZENOH_ENDPOINTS=tcp/localhost:7448
# -----------------------------------------------------------------------------
# Compose profiles — which optional services start
# -----------------------------------------------------------------------------
# Core (postgres, dragonfly, minio*) has no profile and always runs.
# These extras are opt-in; omit any you do not want.
# tools → adminer (DB browser, :8082)
# auth → keycloak (local OAuth, :8180)
# observability → prometheus (:9090)
# bus → zenohd (event-bus router, :7447 — default off)
# artifacts → reductstore (artifact/blob REST tier, :8383 — local disk)
# artifacts-s3 → reductstore (same tier, MinIO behind it — needs a licence)
#
# `artifacts` and `artifacts-s3` are mutually exclusive: same host port, two
# backings of one tier. `artifacts-s3` needs a ReductStore Pro licence key at
# .secrets/reductstore-license.key — see .env.example for the full note.
#
# Unset means core only — a noticeably lighter stack.
# COMPOSE_PROFILES=tools,auth,observability
# -----------------------------------------------------------------------------
# Dragonfly sizing
# -----------------------------------------------------------------------------
# CONSTRAINT: maxmemory must be >= 256MiB per thread, or Dragonfly refuses to
# start and the container exits 1 ("There are N threads, so X MiB are
# required"). Raise both together — 4 threads needs at least 1024mb.
# DRAGONFLY_MAXMEMORY=512mb
# DRAGONFLY_THREADS=2
MERIDIAN_CONTROL_LOGIN_CODE_TTL_SECONDS=3600