Skip to content

environment-manager

The Environment manager. An Environment is a per-tenant deployment context: public domain(s), DNS mode, data residency, backup retention, and legal processor. Its network zone is not in the environment file: the zone names the environment (see "Env ↔ service zone"). Environments live at config/environments/<name>.json and replace the legacy configuration.json .tappaas.variants construct.

What it owns

config/environments/*.json (default ${TAPPAAS_CONFIG:-/home/tappaas/config}/environments/), validated against environment-fields.json. Mandatory fields: name, displayName, ownerOrg. domains is optional in the schema, though bootstrap seeds it on mgmt and the default environment alike when a site domain is known; domains.dnsMode defaults to per-service. The schema is additionalProperties:false, so an authored tlsCertRefid is rejected — a cert refid is runtime state, not authored config.

The environment-manager CLI (ADR-007)

A first-pass port presents the standardized ADR-007 verbs on the environment entity. It is a thin orchestration boundary — it owns config/environments/*.json and shells out to the plane/module managers for reconcile; it reimplements no plane logic (exactly as identity-manager shells out to authentik-controller). The bash scripts below stay live.

environment-manager list [--json] [--config-dir DIR]
environment-manager show <env> [--json] [--config-dir DIR]
environment-manager validate [<file|dir>] [--config-dir DIR]
environment-manager add [<env>] [--name N] [--domain D] [--owner ORG]
                        [--zone Z] [--display D] [--dns-mode M] [--force] [--config-dir DIR]
environment-manager modify <env> [--domain D] [--owner ORG] [--zone Z]
                        [--display D] [--dns-mode M] [--config-dir DIR]
environment-manager delete <env> [--force] [--check] [--config-dir DIR]
environment-manager reconcile <env> [--deep] [--apply] [--config-dir DIR]
                                   [--skip-network]
Verb Behaviour
list Enumerate environments. --json emits the full objects as a JSON array; default prints name (zone …; shared …) per line — its own zone, then any shared service zones serving it.
show <env> One environment in detail (canonical pretty JSON; --json = compact).
validate [<file/dir>] The canonical schema + reference gate, implemented natively (src/validate.ts interprets environment-fields.json in-process — the former validate-environment.sh is retired). Checks full schema conformance (additionalProperties:false, pattern/enum/minLength, the tlsCertRefid rejection) and reference integrity (exactly one own zone in zones.json, ownerOrg in the organizations). See "The validate gate" below.
add Create an environment (writes validated config). With no positional <env> it seeds the minimal set (mgmt + the default <N>) — the bootstrap that replaced the retired create-minimal-environments.sh; --name <N> gives the system name explicitly, else it derives from site.json '.name' (see "The minimal-set bootstrap" below). With a positional <env> it creates that single env. --owner defaults to the first org under identities/organizations/; --zone defaults to <env>.
modify <env> Change an existing environment (preserves un-flagged fields; writes validated config).

--dns-mode <per-service\|wildcard> (on add/modify) sets domains.dnsMode — the cert strategy: per-service (default; Caddy per-host HTTP-01) or wildcard (one *.<primary> OPNsense-ACME cert for the environment). Validated against the schema enum; this closes the last field that previously required a hand-edit.

dnsMode is the CERTIFICATE strategy and nothing else (ADR-021 D4). It does not decide the split-horizon address — that is always the DMZ gateway, from network-manager split-horizon-target (D2/D5) — and it does not dictate the Unbound record shape either. The two are independent, and since every record carries the same address, * and per-host entries resolve identically:

Certificate Record shape Verdict
wildcard wildcard * valid — fewest records
wildcard per-service valid, often preferable — explicit records, no redirect-zone apex constraints (#474), no collision pruning (#505), and a name that is not published simply does not resolve (a wildcard answers for every name under the domain, including ones no Caddy handler serves)
per-service per-service valid — the default
per-service wildcard valid but pointless — publishes names Caddy cannot serve

A domain with no public DNS record is a supported configuration, not an error (ADR-021 R3): ACME cannot issue for it, so reconcile plans no wildcard record and reports it as a note, not a warning. Services stay reachable at <vmname>.<zone>.internal, without TLS and without the identity gate. | delete <env> | Remove an environment file — guard-railed (see below). | | reconcile <env> | Converge the environment → live. --apply commits (default = preview). --deep cascades (see below). Also materializes a missing service zone and hard-errors on a wrongly-typed one — see "Env ↔ service zone" below. |

Reconcile cascade

Env ↔ service zone

An Environment has exactly one service zone, by default the zone named like the environment. The link is written on the zone, not in the environment file: the Service zone's environment field names the environment (for mgmt, the Management zone mgmt names it). validate refuses an environment that no Service or Management zone names, or that two name. network-manager writes zones.json; environment-manager asks it to:

  • add <env> [--zone Z] writes the environment, then binds Z (default: a zone named like the environment) to it with network-manager bind Z --environment <env>. Z must be a Service zone that is no other environment's zone. If the bind fails, the environment file is put back as it was.
  • add <env> --create-zone authors Z as a Service zone naming the environment when it does not exist — one command yields a working pair. Opt-in on purpose: default-on would let a typo'd --zone silently mint a stray zone.
  • modify <env> --zone Z moves the environment: the old zone's link is cleared and Z is bound to it.
  • delete <env> clears the link on every zone naming the environment: its own zone stays as a zone, and the client and IoT zones that served it stop reaching it (a zone serving several environments keeps the others).
  • reconcile <env> finds the zone naming the environment. When none does and no zone of its name exists, it plans the zone's creation and authors it before the network pass. When two zones name it, or a zone of its name exists in another role (a client zone that serves it, say), that is a hard error in both preview and apply; give the environment its zone with modify <env> --zone <serviceZone>.

A client, IoT or guest zone names the environment it serves in the same field — or a list, when it serves several (network-manager bind <zone> --environment <env> [--add]); that does not make it the environment's zone.

A Service zone naming a list of environments is a shared service zone: it serves each environment it lists and is the own zone of none. show names it on a shared row under the own zone, and list after the own zone; a module of the environment may be installed there with --zone0 <zone>, but add and modify --zone refuse it as the environment's zone. validate counts only the own zone, so an environment served by a shared zone alone has no zone and is refused. See network-manager's ZONES.md.

environment-manager add acme --create-zone          # env + service zone 'acme'
environment-manager add acme --zone shared --create-zone
network-manager reconcile --apply                   # converge the new zone

Ownership boundary (unchanged). network-manager remains the sole writer of zones.json; environment-manager requests zone creation across the existing client seam (network-manager add <Z> --archetype service), never editing zones itself. The service archetype means the new zone lands tier-correct rather than with bare defaults.

  • Shallow (reconcile <env>): shell out to network-manager reconcile [--apply]. This pass is system-wide, not scoped to the environment — network-manager has no zone or environment filter, so it converges every zone on every plane; <env>'s zone is simply part of it. The plan tags the action [system-wide] so the label matches what actually happens.
  • Deep (reconcile <env> --deep): the above plus every deployed module that consumes this environment — module-manager reconcile <module> [--apply] per module. Consuming modules are enumerated as the deployed config/*.json files whose .environment field equals <env>. Each reconcile is idempotent, so re-touching the shared network is harmless.
  • --skip-network: drop the network action when the caller has already run the system-wide pass. Reconciling N environments otherwise repeats one identical whole-platform operation N times — which is what site-manager reconcile --deep used to do.

delete guard rails

delete refuses (exit 1) when, without --force:

  • the target is the reserved management environment mgmt, or
  • the target is the default <N> environment (= site.json '.name'), or
  • one or more deployed modules still consume the environment (those modules are listed in the error).

The add minimal-set bootstrap is the single owner of the two bootstrap files; --force overrides the guard rails for the rare deliberate removal.

What delete removes (#790)

Besides the environment file, what add, reconcile and acme-setup.sh created for it: the Unbound *.<domain> override(s), its cert-refids.json entry, and its ACME certificate with the Trust certificate (acme-manager delete). Nothing shared: a domain another environment still uses keeps its override and certificate, and the ACME account, validation and action stay. --check prints the plan and changes nothing. The output ends with what only you can remove: the public DNS records at your DNS provider (#383). A step that fails is reported as NOT removed, with the command to finish it by hand.

The validate gate

environment-manager validate (also reachable as the P10 verb script validate.sh) validates environment files against src/foundation/schemas/environment-fields.json plus reference integrity:

environment-manager validate [FILE|DIR] [--schema-dir PATH] [--config-dir DIR] [--zones FILE] [--quiet]
  • FILE|DIR — an environment .json or a directory of them (default $TAPPAAS_CONFIG/environments).
  • --schema-dir PATH — directory holding environment-fields.json (default: $SCHEMA_DIR, else derived from the checkout).
  • --config-dir DIR — config dir for the zones.json + organizations lookup.
  • --zones FILE — path to zones.json.
  • --quiet — errors/warnings only.

The schema stays the single source of truth: src/validate.ts interprets it at runtime (the small draft-2020-12 subset it uses), and fails loudly if the schema ever grows a keyword the interpreter does not implement — so the gate can never silently under-validate. Exit codes: 0 = valid (warnings allowed), 1 = validation errors. The former validate-environment.sh bash implementation (Python jsonschema + jq fallback) is retired.

The minimal-set bootstrap (add with no <env>)

Creates the two always-required environments: mgmt.json (zone mgmt, no domains) and the default tenant environment <N>.json, named after the TAPPaaS system name <N>. Neither names a zone: the shipped zones.json carries environment: "mgmt" on mgmt, and network-manager init names <N> on the zone it renames to <N>. Idempotent (existing files are left untouched without --force); never deletes operator files (a stale legacy default.json is noted, not removed).

environment-manager add [--name <N>] [--domain <D>] [--config-dir DIR] [--force]
  • --name <N> — system name (= default zone & env name). If omitted, derived from site.json '.name'.
  • --domain <D> — the default env's domains.primary (omitted ⇒ no domain yet).
  • --config-dir DIR — config directory (default $TAPPAAS_CONFIG); environments land in <config-dir>/environments.
  • --force — overwrite existing mgmt.json / <N>.json.
environment-manager add --name acme

This is the single owner of the two bootstrap files (install.sh calls it) — downstream steps consume, they do not re-author them.

Retired tooling (ADR-007 Phase D)

The legacy ADR-005 variant registry is retired. The following scripts have been removed — environments authored under config/environments/*.json are the single source of truth, and modules deploy via install-module.sh <module> --environment <env> (--variant is a deprecated alias for --environment):

  • variant-manager.sh (managed configuration.json .tappaas.variants)
  • migrate-variants.sh / migrate-variants-to-environments.sh
  • migrate-to-variants.sh

Also retired (ADR-007 post-implementation refactor): validate-environment.sh — the environment-manager validate verb is the schema + reference gate now (same flags, same exit codes; see "The validate gate" above) — and create-minimal-environments.sh (Phase 8.1) — environment-manager add with no positional <env> is the minimal-set bootstrap now (same --name/--domain/ --config-dir/--force flags; the unused --out-dir was dropped).

To create or change an environment, author/edit its config/environments/<env>.json file (validated by environment-manager validate); to create its dedicated network zone use network-manager zone add <env> --from-zone <src> --variant <env>.