refactor(env): explicit defaults.env with placeholders; drop auto-injection
A service/job's env is now exactly its defaults.env — castle injects no hidden
convention vars. Values support ${port}/${data_dir}/${name} placeholders
(resolved at deploy, alongside ${secret:…}), so a program's own env var names
map to castle's computed values without hardcoding.
Why: the auto-injected <PREFIX>_PORT/<PREFIX>_DATA_DIR were a guess at the
program's env names — right for castle-scaffolded services, dead weight for
adopted ones (lakehouse carried two dead vars; notification-bridge/backup jobs
too). They also weren't visible in the config editor (computed at deploy), which
was the source of the 'four env vars but the UI shows none' mystery.
- core: resolve_env_vars gains a context (${port}/${data_dir}/${name});
deploy builds env from defaults.env only — no <PREFIX>_* injection, no
port_env. Removed the port_env field and the dead _env_prefix helper.
- cli: 'service/job create' gains repeatable --env KEY=VALUE (replaces
--port-env); 'program create' scaffolds <PREFIX>_PORT/_DATA_DIR: ${…} for new
daemons.
- app: removed the 'Port env' field; the Environment editor (defaults.env) is
the single place, with a placeholder hint.
- live migration: central-context/castle-api/power-graph/protonmail mapped their
real vars explicitly; lakehouse → just LAKEHOUSED_DAEMON_PORT: ${port}, data
stays in ~/.lakehoused. Verified all services healthy on their ports, dead
vars gone, zero failed units.
- docs: registry.md/design.md/stack guides + findings updated to the explicit
model.
core 94 / cli 24 / api 52 green; ruff + app build clean.
This commit is contained in:
@@ -38,7 +38,6 @@ export function CreateDeploymentForm({
|
||||
const [runner, setRunner] = useState(prefill?.runner ?? "python")
|
||||
const [runTarget, setRunTarget] = useState(prefill?.runTarget ?? prefill?.name ?? "")
|
||||
const [port, setPort] = useState("")
|
||||
const [portEnv, setPortEnv] = useState("")
|
||||
const [health, setHealth] = useState("/health")
|
||||
const [path, setPath] = useState("")
|
||||
const [host, setHost] = useState("")
|
||||
@@ -71,7 +70,7 @@ export function CreateDeploymentForm({
|
||||
if (port) {
|
||||
base.expose = {
|
||||
http: {
|
||||
internal: { port: parseInt(port, 10), ...(portEnv ? { port_env: portEnv } : {}) },
|
||||
internal: { port: parseInt(port, 10) },
|
||||
...(health ? { health_path: health } : {}),
|
||||
},
|
||||
}
|
||||
@@ -160,7 +159,6 @@ export function CreateDeploymentForm({
|
||||
{kind === "service" ? (
|
||||
<>
|
||||
<TextField label="Port" value={port} onChange={setPort} width="w-32" mono placeholder="9001" />
|
||||
<TextField label="Port env" value={portEnv} onChange={setPortEnv} width="w-64" mono placeholder="(custom port var, optional)" />
|
||||
<TextField label="Health path" value={health} onChange={setHealth} width="w-48" mono />
|
||||
<TextField label="Proxy path" value={path} onChange={setPath} width="w-48" mono placeholder={`/${name || "name"}`} />
|
||||
<TextField label="Proxy host" value={host} onChange={setHost} mono placeholder="my-service.lan (optional)" />
|
||||
|
||||
@@ -28,7 +28,6 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
(run.program as string) ?? ((run.argv as string[]) ?? []).join(" "),
|
||||
)
|
||||
const [port, setPort] = useState(internal.port != null ? String(internal.port) : "")
|
||||
const [portEnv, setPortEnv] = useState((internal.port_env as string) ?? "")
|
||||
const [health, setHealth] = useState((httpExpose.health_path as string) ?? "")
|
||||
const [proxyPath, setProxyPath] = useState((caddy.path_prefix as string) ?? "")
|
||||
const [proxyHost, setProxyHost] = useState((caddy.host as string) ?? "")
|
||||
@@ -55,10 +54,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
if (port) {
|
||||
config.expose = {
|
||||
http: {
|
||||
internal: {
|
||||
port: parseInt(port, 10),
|
||||
...(portEnv ? { port_env: portEnv } : {}),
|
||||
},
|
||||
internal: { port: parseInt(port, 10) },
|
||||
...(health ? { health_path: health } : {}),
|
||||
},
|
||||
}
|
||||
@@ -101,14 +97,6 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
/>
|
||||
</Field>
|
||||
<TextField label="Port" value={port} onChange={setPort} width="w-32" mono placeholder="9001" />
|
||||
<TextField
|
||||
label="Port env"
|
||||
value={portEnv}
|
||||
onChange={setPortEnv}
|
||||
width="w-64"
|
||||
mono
|
||||
placeholder="(only if the program reads a custom var)"
|
||||
/>
|
||||
<TextField label="Health path" value={health} onChange={setHealth} width="w-48" mono placeholder="/health" />
|
||||
<TextField label="Proxy path" value={proxyPath} onChange={setProxyPath} width="w-48" mono placeholder="/my-service" />
|
||||
<TextField label="Proxy host" value={proxyHost} onChange={setProxyHost} mono placeholder="my-service.lan" />
|
||||
|
||||
@@ -71,6 +71,12 @@ export function useEnvSecrets(initial: Record<string, string>) {
|
||||
<div className="space-y-4">
|
||||
<Field label="Environment">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-[var(--muted)]">
|
||||
Use <code className="font-mono">${"{port}"}</code>,{" "}
|
||||
<code className="font-mono">${"{data_dir}"}</code>,{" "}
|
||||
<code className="font-mono">${"{name}"}</code> for castle's computed values,
|
||||
and <code className="font-mono">${"{secret:NAME}"}</code> for secrets.
|
||||
</p>
|
||||
{Object.entries(env).map(([key, val]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<input value={key} readOnly className={`w-56 ${INPUT} text-xs text-[var(--muted)]`} />
|
||||
|
||||
@@ -8,6 +8,7 @@ import subprocess
|
||||
from castle_cli.config import REPOS_DIR, load_config, save_config
|
||||
from castle_cli.manifest import (
|
||||
CaddySpec,
|
||||
DefaultsSpec,
|
||||
ExposeSpec,
|
||||
HttpExposeSpec,
|
||||
HttpInternal,
|
||||
@@ -92,6 +93,7 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
behavior=behavior,
|
||||
)
|
||||
if behavior == "daemon":
|
||||
prefix = name.replace("-", "_").upper()
|
||||
config.services[name] = ServiceSpec(
|
||||
id=name,
|
||||
program=name,
|
||||
@@ -104,6 +106,11 @@ def run_create(args: argparse.Namespace) -> int:
|
||||
),
|
||||
proxy=ProxySpec(caddy=CaddySpec(path_prefix=f"/{name}")),
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
# python-fastapi scaffold reads env_prefix MY_SERVICE_ — map castle's
|
||||
# computed port/data dir to those vars (explicit, no hidden injection).
|
||||
defaults=DefaultsSpec(
|
||||
env={f"{prefix}_PORT": "${port}", f"{prefix}_DATA_DIR": "${data_dir}"}
|
||||
),
|
||||
)
|
||||
|
||||
save_config(config)
|
||||
|
||||
@@ -12,6 +12,7 @@ import argparse
|
||||
from castle_cli.config import load_config, save_config
|
||||
from castle_cli.manifest import (
|
||||
CaddySpec,
|
||||
DefaultsSpec,
|
||||
ExposeSpec,
|
||||
HttpExposeSpec,
|
||||
HttpInternal,
|
||||
@@ -25,6 +26,17 @@ from castle_cli.manifest import (
|
||||
)
|
||||
|
||||
|
||||
def _defaults(env_args: list[str] | None) -> DefaultsSpec | None:
|
||||
"""Parse repeated --env KEY=VALUE into a DefaultsSpec, or None."""
|
||||
if not env_args:
|
||||
return None
|
||||
env: dict[str, str] = {}
|
||||
for item in env_args:
|
||||
key, _, value = item.partition("=")
|
||||
env[key.strip()] = value
|
||||
return DefaultsSpec(env=env)
|
||||
|
||||
|
||||
def _run_spec(runner: str, target: str, name: str) -> RunPython | RunCommand:
|
||||
if runner == "command":
|
||||
return RunCommand(runner="command", argv=target.split() or [name])
|
||||
@@ -54,7 +66,7 @@ def run_service_create(args: argparse.Namespace) -> int:
|
||||
if args.port is not None:
|
||||
expose = ExposeSpec(
|
||||
http=HttpExposeSpec(
|
||||
internal=HttpInternal(port=args.port, port_env=args.port_env),
|
||||
internal=HttpInternal(port=args.port),
|
||||
health_path=args.health,
|
||||
)
|
||||
)
|
||||
@@ -76,13 +88,14 @@ def run_service_create(args: argparse.Namespace) -> int:
|
||||
expose=expose,
|
||||
proxy=proxy,
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
defaults=_defaults(args.env),
|
||||
)
|
||||
save_config(config)
|
||||
|
||||
print(f"Created service '{name}'.")
|
||||
print(f" runs: {args.runner} ({args.run or args.program or name})")
|
||||
if expose:
|
||||
print(f" port: {args.port}" + (f" (env: {args.port_env})" if args.port_env else ""))
|
||||
print(f" port: {args.port}")
|
||||
if proxy and proxy.caddy:
|
||||
if proxy.caddy.path_prefix:
|
||||
print(f" proxy: {proxy.caddy.path_prefix}")
|
||||
@@ -109,6 +122,7 @@ def run_job_create(args: argparse.Namespace) -> int:
|
||||
run=run,
|
||||
schedule=args.schedule,
|
||||
manage=ManageSpec(systemd=SystemdSpec()),
|
||||
defaults=_defaults(args.env),
|
||||
)
|
||||
save_config(config)
|
||||
|
||||
|
||||
@@ -77,12 +77,17 @@ def _add_service_create(sub: argparse._SubParsersAction, kind: str) -> None:
|
||||
p.add_argument("--description", default="", help="Description")
|
||||
p.add_argument("--run", help="Console script / command to run (default: --program or name)")
|
||||
p.add_argument("--runner", choices=["python", "command"], default="python")
|
||||
p.add_argument(
|
||||
"--env",
|
||||
action="append",
|
||||
metavar="KEY=VALUE",
|
||||
help="Env var for the program (repeatable). Use ${port}/${data_dir}/${name} placeholders.",
|
||||
)
|
||||
if kind == "service":
|
||||
p.add_argument("--port", type=int, help="HTTP port")
|
||||
p.add_argument("--health", default="/health", help="Health path (default: /health)")
|
||||
p.add_argument("--path", help="Gateway proxy prefix (default: /<name>)")
|
||||
p.add_argument("--host", help="Route by hostname instead of a path prefix")
|
||||
p.add_argument("--port-env", help="Env var the program reads for its port")
|
||||
p.add_argument("--no-proxy", action="store_true", help="Don't add a gateway route")
|
||||
else:
|
||||
p.add_argument("--schedule", default="0 2 * * *", help="Cron schedule (default: 0 2 * * *)")
|
||||
|
||||
@@ -141,16 +141,27 @@ class CastleConfig:
|
||||
}
|
||||
|
||||
|
||||
def resolve_env_vars(env: dict[str, str]) -> dict[str, str]:
|
||||
"""Resolve ${secret:NAME} references in env values."""
|
||||
def resolve_env_vars(
|
||||
env: dict[str, str], context: dict[str, str] | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Resolve placeholders in env values.
|
||||
|
||||
- ``${secret:NAME}`` reads `~/.castle/secrets/NAME`.
|
||||
- ``${port}`` / ``${data_dir}`` / ``${name}`` (and anything else in
|
||||
``context``) expand to castle's computed values, so a service maps them to
|
||||
whatever env var its program reads (e.g. ``MY_PORT: ${port}``) without
|
||||
hardcoding or castle silently injecting a guessed var name.
|
||||
"""
|
||||
context = context or {}
|
||||
resolved = {}
|
||||
for key, value in env.items():
|
||||
|
||||
def replace_var(match: re.Match[str]) -> str:
|
||||
ref = match.group(1)
|
||||
if ref.startswith("secret:"):
|
||||
secret_name = ref[7:]
|
||||
return _read_secret(secret_name)
|
||||
return _read_secret(ref[7:])
|
||||
if ref in context:
|
||||
return context[ref]
|
||||
return match.group(0)
|
||||
|
||||
resolved[key] = re.sub(r"\$\{([^}]+)\}", replace_var, value)
|
||||
|
||||
@@ -157,9 +157,12 @@ def _reload_gateway(messages: list[str]) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _env_prefix(name: str) -> str:
|
||||
"""Derive env var prefix from name: central-context → CENTRAL_CONTEXT."""
|
||||
return name.replace("-", "_").upper()
|
||||
def _env_context(name: str, config_key: str, port: int | None) -> dict[str, str]:
|
||||
"""Placeholder values for defaults.env: ${name}/${data_dir}/${port}."""
|
||||
ctx = {"name": name, "data_dir": str(DATA_DIR / config_key)}
|
||||
if port is not None:
|
||||
ctx["port"] = str(port)
|
||||
return ctx
|
||||
|
||||
|
||||
def _resolve_description(config: CastleConfig, spec: ServiceSpec | JobSpec) -> str | None:
|
||||
@@ -176,41 +179,26 @@ def _build_deployed_service(
|
||||
) -> Deployment:
|
||||
"""Build a Deployment from a ServiceSpec."""
|
||||
run = svc.run
|
||||
# Env prefix and data dir are keyed by the program (component) the service
|
||||
# runs, not the service name — that's the prefix the program's settings read
|
||||
# (e.g. job `protonmail-sync` runs program `protonmail` → PROTONMAIL_DATA_DIR).
|
||||
# Falls back to the service name when no component is referenced.
|
||||
# The data-dir placeholder is keyed by the program the service runs, not the
|
||||
# service name (e.g. job `protonmail-sync` runs program `protonmail` →
|
||||
# /data/castle/protonmail). Falls back to the service name.
|
||||
config_key = svc.program or name
|
||||
prefix = _env_prefix(config_key)
|
||||
env: dict[str, str] = {}
|
||||
|
||||
# Data dir convention (for managed services). Skipped for container runners:
|
||||
# they use volume mounts, not a data-dir env var, and the injected name can
|
||||
# collide with an image's own env namespace (e.g. neo4j claims NEO4J_*).
|
||||
managed = run.runner != "remote"
|
||||
if svc.manage and svc.manage.systemd and not svc.manage.systemd.enable:
|
||||
managed = False
|
||||
if managed and run.runner != "container":
|
||||
env[f"{prefix}_DATA_DIR"] = str(DATA_DIR / config_key)
|
||||
|
||||
# Port convention (if exposed)
|
||||
port = None
|
||||
health_path = None
|
||||
if svc.expose and svc.expose.http:
|
||||
port = svc.expose.http.internal.port
|
||||
env[f"{prefix}_PORT"] = str(port)
|
||||
# If the program reads a non-convention port var, set that too so castle
|
||||
# actually controls the bind (e.g. adopted lakehoused → LAKEHOUSED_DAEMON_PORT).
|
||||
if svc.expose.http.internal.port_env:
|
||||
env[svc.expose.http.internal.port_env] = str(port)
|
||||
health_path = svc.expose.http.health_path
|
||||
|
||||
# Merge defaults.env (overrides conventions)
|
||||
if svc.defaults and svc.defaults.env:
|
||||
env.update(svc.defaults.env)
|
||||
|
||||
# Resolve secrets
|
||||
env = resolve_env_vars(env)
|
||||
# Env is exactly what's declared in defaults.env — no hidden convention
|
||||
# injection. ${port}/${data_dir}/${name} let the program's own env var names
|
||||
# map to castle's computed values without hardcoding them.
|
||||
env = dict(svc.defaults.env) if (svc.defaults and svc.defaults.env) else {}
|
||||
env = resolve_env_vars(env, _env_context(name, config_key, port))
|
||||
|
||||
# Ensure python tool is installed before resolving binary
|
||||
_ensure_python_tool(config, svc.program, messages)
|
||||
@@ -260,18 +248,11 @@ def _build_deployed_job(
|
||||
) -> Deployment:
|
||||
"""Build a Deployment from a JobSpec."""
|
||||
run = job.run
|
||||
# Keyed by the program (component) the job runs, not the job name — see
|
||||
# _build_deployed_service for rationale. Falls back to the job name.
|
||||
# ${data_dir} is keyed by the program the job runs, not the job name — see
|
||||
# _build_deployed_service. Falls back to the job name.
|
||||
config_key = job.program or name
|
||||
prefix = _env_prefix(config_key)
|
||||
env: dict[str, str] = {}
|
||||
|
||||
env[f"{prefix}_DATA_DIR"] = str(DATA_DIR / config_key)
|
||||
|
||||
if job.defaults and job.defaults.env:
|
||||
env.update(job.defaults.env)
|
||||
|
||||
env = resolve_env_vars(env)
|
||||
env = dict(job.defaults.env) if (job.defaults and job.defaults.env) else {}
|
||||
env = resolve_env_vars(env, _env_context(name, config_key, None))
|
||||
_ensure_python_tool(config, job.program, messages)
|
||||
run_cmd = _build_run_cmd(name, run, env, messages)
|
||||
|
||||
|
||||
@@ -111,12 +111,6 @@ class HttpInternal(BaseModel):
|
||||
host: str = "127.0.0.1"
|
||||
port: int = Field(ge=1, le=65535)
|
||||
unix_socket: str | None = None
|
||||
# Env var the program actually reads for its bind port. Castle's convention
|
||||
# injects <PREFIX>_PORT, but an adopted program may read a different name
|
||||
# (e.g. lakehoused reads LAKEHOUSED_DAEMON_PORT). When set, deploy also sets
|
||||
# this var to `port`, so castle genuinely drives the bind instead of relying
|
||||
# on the program's default happening to match.
|
||||
port_env: str | None = None
|
||||
|
||||
|
||||
class HttpPublic(BaseModel):
|
||||
|
||||
@@ -195,9 +195,9 @@ Services and jobs can reference a program via `program:` for description
|
||||
fallthrough and source code linking. They can also exist independently
|
||||
(e.g., `castle-gateway` runs Caddy — not our software).
|
||||
|
||||
Convention-based env vars (`<PREFIX>_PORT`, `<PREFIX>_DATA_DIR`) are
|
||||
generated automatically during deploy. Only non-convention values need
|
||||
`defaults.env`.
|
||||
A service's env is exactly its `defaults.env` — castle injects nothing
|
||||
implicitly. Values may use `${port}`/`${data_dir}`/`${name}`/`${secret:…}`
|
||||
placeholders, which deploy resolves into the registry's flat `env`.
|
||||
|
||||
**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle deploy`) — Node config:
|
||||
|
||||
@@ -222,8 +222,8 @@ deployed:
|
||||
```
|
||||
|
||||
The node config says what's deployed *here* and with what concrete
|
||||
values. `castle deploy` reads the spec from the repo, generates
|
||||
convention-based env vars, resolves secrets, resolves binary paths,
|
||||
values. `castle deploy` reads the spec from the repo, resolves the
|
||||
`defaults.env` placeholders and secrets, resolves binary paths,
|
||||
and writes the registry. Systemd units and Caddyfile are then generated
|
||||
from the registry — never from the spec directly.
|
||||
|
||||
@@ -521,9 +521,9 @@ What exists today:
|
||||
`$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
|
||||
Caddyfile generated from registry with fully resolved paths. No repo references
|
||||
in runtime artifacts.
|
||||
- **Convention-based env generation** — `castle deploy` auto-generates
|
||||
`<PREFIX>_DATA_DIR=$CASTLE_DATA_DIR/<name>` and `<PREFIX>_PORT` from
|
||||
the manifest. Only non-convention values need `defaults.env`.
|
||||
- **Explicit env with placeholders** — a deployment's env is exactly its
|
||||
`defaults.env`; `castle deploy` resolves `${port}`/`${data_dir}`/`${name}`/
|
||||
`${secret:…}` into concrete values. No hidden convention injection.
|
||||
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
|
||||
- **API** — `castle-api` on port 9020, reads from registry (optional
|
||||
castle.yaml fallback for non-deployed programs)
|
||||
|
||||
@@ -40,10 +40,13 @@ the daemon's built-in default and we declared the same value. Castle's
|
||||
convention assumes a castle-aware program; an adopted one doesn't read
|
||||
`<PREFIX>_PORT`.
|
||||
|
||||
**Fix:** let a service declare which env var carries the port, e.g.
|
||||
`expose.http.port_env: LAKEHOUSED_DAEMON_PORT`, so castle sets *that* to the
|
||||
configured port and genuinely drives the bind. (`defaults.env` is the manual
|
||||
workaround.)
|
||||
**Fixed** (superseding the interim `port_env` field): auto-injection of the
|
||||
convention vars was dropped entirely. A service's env is now exactly its
|
||||
`defaults.env`, with `${port}`/`${data_dir}`/`${name}` placeholders for castle's
|
||||
computed values. lakehouse maps just what it reads —
|
||||
`LAKEHOUSED_DAEMON_PORT: ${port}` — and keeps its own data root, with no dead
|
||||
vars. Castle-native services map their `<PREFIX>_PORT`/`_DATA_DIR` the same way
|
||||
(scaffolded by `castle program create`).
|
||||
|
||||
### #3 — `castle deploy` regenerates the Caddyfile but never reloads Caddy (High, bug)
|
||||
|
||||
|
||||
@@ -255,17 +255,36 @@ manage:
|
||||
exec_reload: "caddy reload ..."
|
||||
```
|
||||
|
||||
### `defaults` — Default environment
|
||||
### `defaults` — Environment
|
||||
|
||||
`defaults.env` is the **single, explicit source** of the env a service/job runs
|
||||
with — what you write here is exactly what lands in the systemd unit. Castle
|
||||
does **not** inject hidden convention vars; whatever env var your program reads
|
||||
for its port, data dir, etc., you map here.
|
||||
|
||||
```yaml
|
||||
expose: { http: { internal: { port: 9001 }, health_path: /health } }
|
||||
defaults:
|
||||
env:
|
||||
MY_SERVICE_PORT: ${port} # the program's own port var ← expose.port
|
||||
MY_SERVICE_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/<name>
|
||||
CENTRAL_CONTEXT_URL: http://localhost:9001
|
||||
API_KEY: ${secret:MY_API_KEY}
|
||||
```
|
||||
|
||||
Castle resolves `${secret:NAME}` by reading `~/.castle/secrets/NAME`.
|
||||
Never store secrets in castle.yaml or project directories.
|
||||
Values may contain placeholders that castle resolves at deploy:
|
||||
|
||||
| Placeholder | Expands to |
|
||||
|-------------|------------|
|
||||
| `${port}` | the service's `expose.http.internal.port` (so it can't drift) |
|
||||
| `${data_dir}` | `$CASTLE_DATA_DIR/<program-or-name>` (the dedicated data volume) |
|
||||
| `${name}` | the deployment name |
|
||||
| `${secret:NAME}` | the contents of `~/.castle/secrets/NAME` |
|
||||
|
||||
Hardcode the values instead if you prefer; the placeholders just save you from
|
||||
repeating castle's computed paths/ports. `castle program create` scaffolds the
|
||||
`${port}`/`${data_dir}` lines for new services. Never store secrets in
|
||||
castle.yaml — use `${secret:…}`.
|
||||
|
||||
## Job blocks
|
||||
|
||||
@@ -431,9 +450,10 @@ variable (both expand `~` and resolve relative paths):
|
||||
|
||||
Defined in `core/src/castle_core/config.py`: `CASTLE_HOME` (with derived
|
||||
`CODE_DIR`, `SECRETS_DIR`, `SPECS_DIR`, `CONTENT_DIR`) and the independent
|
||||
`DATA_DIR` (`CASTLE_DATA_DIR`). `castle deploy` passes each service its data
|
||||
path via the generated `<PREFIX>_DATA_DIR` env var. Systemd unit/timer paths are
|
||||
fixed by systemd's user-unit convention.
|
||||
`DATA_DIR` (`CASTLE_DATA_DIR`). A service reaches its data path by mapping
|
||||
`${data_dir}` (= `$CASTLE_DATA_DIR/<name>`) to the env var its program reads, in
|
||||
`defaults.env`. Systemd unit/timer paths are fixed by systemd's user-unit
|
||||
convention.
|
||||
|
||||
## Manifest models
|
||||
|
||||
|
||||
@@ -129,16 +129,21 @@ services:
|
||||
systemd: {}
|
||||
```
|
||||
|
||||
Convention-based env vars (`MY_SERVICE_DATA_DIR`, `MY_SERVICE_PORT`) are
|
||||
generated automatically by `castle deploy`. Only non-convention values
|
||||
need `defaults.env`:
|
||||
The env a service runs with is exactly what's in `defaults.env` — castle injects
|
||||
nothing implicitly. Map the vars your settings read (above, `env_prefix:
|
||||
"MY_SERVICE_"` → `MY_SERVICE_PORT`/`MY_SERVICE_DATA_DIR`) to castle's computed
|
||||
values with the `${port}`/`${data_dir}` placeholders:
|
||||
|
||||
```yaml
|
||||
defaults:
|
||||
env:
|
||||
MY_SERVICE_PORT: ${port} # = expose.http.internal.port
|
||||
MY_SERVICE_DATA_DIR: ${data_dir} # = $CASTLE_DATA_DIR/my-service
|
||||
CENTRAL_CONTEXT_URL: http://localhost:9001
|
||||
```
|
||||
|
||||
`castle program create` scaffolds the `${port}`/`${data_dir}` lines for you.
|
||||
|
||||
## Application entry point
|
||||
|
||||
```python
|
||||
@@ -305,8 +310,9 @@ $CASTLE_DATA_DIR/my-service/ # default /data/castle/my-service/
|
||||
└── item-name.meta.json
|
||||
```
|
||||
|
||||
The service receives this path via the generated `<PREFIX>_DATA_DIR` env var —
|
||||
it never hardcodes it. Use the `data_dir` setting from your config.
|
||||
The service receives this path via its `<PREFIX>_DATA_DIR` env var, which
|
||||
`defaults.env` maps from `${data_dir}` — it never hardcodes it. Use the
|
||||
`data_dir` setting from your config.
|
||||
|
||||
```python
|
||||
# storage.py
|
||||
|
||||
Reference in New Issue
Block a user