Add ${public_url} defaults.env placeholder for gateway-facing origin
Services often need their own public origin — the URL a browser loads them
at through the gateway — to populate CORS/WebSocket/secure-context allowlists.
Previously this had to be hardcoded in the app's own config, which silently
broke when the subdomain-routing migration changed the address (openclaw's
Control UI rejected its new origin).
${public_url} resolves at deploy to the service's gateway-facing base URL:
https://<name>.<gateway.domain> when exposed under tls: acme, else the
node-local http://localhost:<port>. It tracks gateway.domain, so a domain
change needs no per-app edit. Jobs (not exposed) don't get it.
Matches the TLS mode as the raw lowercased string == "acme", the same way the
Caddyfile generator does (the TLSMode enum is stale).
This commit is contained in:
@@ -211,11 +211,35 @@ def _reload_gateway(messages: list[str]) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _env_context(name: str, config_key: str, port: int | None) -> dict[str, str]:
|
||||
"""Placeholder values for defaults.env: ${name}/${data_dir}/${port}."""
|
||||
def _public_url(
|
||||
config: CastleConfig, name: str, exposed: bool, port: int | None
|
||||
) -> str | None:
|
||||
"""The service's publicly-reachable base URL — the ``${public_url}`` placeholder.
|
||||
|
||||
When the service is exposed through the gateway under acme TLS, this is its
|
||||
trusted subdomain ``https://<name>.<gateway.domain>`` — the origin an app must
|
||||
allowlist for CORS/WebSocket/secure-context to work behind the gateway. It
|
||||
tracks ``gateway.domain`` automatically, so a domain change needs no app edit.
|
||||
Off mode / port-only falls back to the node-local ``http://localhost:<port>``;
|
||||
``None`` when there's no port to reach it on (nothing to interpolate).
|
||||
"""
|
||||
gw = config.gateway
|
||||
if exposed and str(gw.tls or "").lower() == "acme" and gw.domain:
|
||||
return f"https://{name}.{gw.domain}"
|
||||
if port is not None:
|
||||
return f"http://localhost:{port}"
|
||||
return None
|
||||
|
||||
|
||||
def _env_context(
|
||||
name: str, config_key: str, port: int | None, public_url: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Placeholder values for defaults.env: ${name}/${data_dir}/${port}/${public_url}."""
|
||||
ctx = {"name": name, "data_dir": str(DATA_DIR / config_key)}
|
||||
if port is not None:
|
||||
ctx["port"] = str(port)
|
||||
if public_url is not None:
|
||||
ctx["public_url"] = public_url
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -276,12 +300,16 @@ def _build_deployed_service(
|
||||
health_path = svc.expose.http.health_path
|
||||
|
||||
# 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. Secret-bearing vars
|
||||
# injection. ${port}/${data_dir}/${name}/${public_url} let the program's own
|
||||
# env var names map to castle's computed values without hardcoding them.
|
||||
# Secret-bearing vars
|
||||
# are split out so they never land in the unit file or process argv — they're
|
||||
# written to a mode-0600 env file referenced via EnvironmentFile=/--env-file.
|
||||
raw_env = dict(svc.defaults.env) if (svc.defaults and svc.defaults.env) else {}
|
||||
env, secret_env = resolve_env_split(raw_env, _env_context(name, config_key, port))
|
||||
public_url = _public_url(config, name, expose, port)
|
||||
env, secret_env = resolve_env_split(
|
||||
raw_env, _env_context(name, config_key, port, public_url)
|
||||
)
|
||||
secret_env_file = _write_secret_env_file(name, secret_env)
|
||||
|
||||
# `command`-runner services resolve a tool on PATH → ensure it's installed.
|
||||
|
||||
@@ -469,6 +469,7 @@ Values may contain placeholders that castle resolves at deploy:
|
||||
| `${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 |
|
||||
| `${public_url}` | the service's gateway-facing base URL — `https://<name>.<domain>` when exposed under `tls: acme`, else the node-local `http://localhost:<port>`. The origin an app allowlists (CORS/WebSocket/secure-context); tracks `gateway.domain`, so a domain change needs no app edit. |
|
||||
| `${secret:NAME}` | the contents of `~/.castle/secrets/NAME` |
|
||||
|
||||
Hardcode the values instead if you prefer; the placeholders just save you from
|
||||
|
||||
Reference in New Issue
Block a user