mirror of
https://github.com/furyhawk/api_gateway.git
synced 2026-07-20 09:47:59 +00:00
feat: enhance gateway configuration to support environment variable placeholders for upstream host
This commit is contained in:
@@ -39,7 +39,7 @@ uv sync
|
||||
|
||||
3. Choose a gateway profile:
|
||||
- `config/gateway.yaml` targets a locally running companion backend on `127.0.0.1:8068`.
|
||||
- `config/gateway.container.yaml` targets the companion backend inside the bundled compose network, with default `gateway_port: 8067`.
|
||||
- `config/gateway.container.yaml` uses `GATEWAY_UPSTREAM_HOST` for host-container networking and defaults to `host.docker.internal` (set `GATEWAY_UPSTREAM_HOST=host.containers.internal` for Podman), with default `gateway_port: 8067`.
|
||||
4. Run the gateway with the managed environment:
|
||||
|
||||
```bash
|
||||
@@ -215,6 +215,7 @@ make stop
|
||||
- `CONTAINER_ENGINE=docker` (or `podman`)
|
||||
- `PORT=8067`
|
||||
- `GATEWAY_PORT=8067`
|
||||
- `GATEWAY_UPSTREAM_HOST=host.docker.internal` (set to `host.containers.internal` for Podman host networking)
|
||||
- `IMAGE_NAME=openapi-api-gateway`
|
||||
- `IMAGE_TAG=latest`
|
||||
- `ENV_FILE=.env`
|
||||
|
||||
@@ -13,7 +13,7 @@ settings:
|
||||
|
||||
upstreams:
|
||||
lta_datamall:
|
||||
base_url: http://host.docker.internal/ # Podman: use http://host.containers.internal/
|
||||
base_url: http://${GATEWAY_UPSTREAM_HOST:-host.docker.internal}/
|
||||
port: 8068
|
||||
timeout_seconds: 20
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
@@ -87,6 +90,32 @@ class GatewayConfig(BaseModel):
|
||||
routes: list[RouteConfig]
|
||||
|
||||
|
||||
_ENV_PLACEHOLDER_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")
|
||||
|
||||
|
||||
def _expand_env_placeholders(value: str) -> str:
|
||||
def replacer(match: re.Match[str]) -> str:
|
||||
var_name = match.group(1)
|
||||
default_value = match.group(2)
|
||||
if var_name in os.environ:
|
||||
return os.environ[var_name]
|
||||
if default_value is not None:
|
||||
return default_value
|
||||
raise ValueError(f"Missing required environment variable: {var_name}")
|
||||
|
||||
return _ENV_PLACEHOLDER_PATTERN.sub(replacer, value)
|
||||
|
||||
|
||||
def _resolve_env_placeholders(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
return _expand_env_placeholders(value)
|
||||
if isinstance(value, list):
|
||||
return [_resolve_env_placeholders(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _resolve_env_placeholders(item) for key, item in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
def load_gateway_config(config_path: str | Path) -> GatewayConfig:
|
||||
path = Path(config_path)
|
||||
if not path.exists():
|
||||
@@ -96,6 +125,8 @@ def load_gateway_config(config_path: str | Path) -> GatewayConfig:
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("Gateway config must be a YAML object")
|
||||
|
||||
raw = _resolve_env_placeholders(raw)
|
||||
|
||||
try:
|
||||
config = GatewayConfig.model_validate(raw)
|
||||
except ValidationError as exc:
|
||||
|
||||
@@ -74,6 +74,68 @@ routes:
|
||||
load_gateway_config(config_file)
|
||||
|
||||
|
||||
def test_load_gateway_config_expands_env_with_default(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("GATEWAY_UPSTREAM_HOST", raising=False)
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: http://${GATEWAY_UPSTREAM_HOST:-host.docker.internal}/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = load_gateway_config(config_file)
|
||||
|
||||
assert str(config.upstreams["demo"].base_url) == "http://host.docker.internal/"
|
||||
|
||||
|
||||
def test_load_gateway_config_expands_env_override(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("GATEWAY_UPSTREAM_HOST", "host.containers.internal")
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: http://${GATEWAY_UPSTREAM_HOST:-host.docker.internal}/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = load_gateway_config(config_file)
|
||||
|
||||
assert str(config.upstreams["demo"].base_url) == "http://host.containers.internal/"
|
||||
|
||||
|
||||
def test_load_gateway_config_missing_required_env_var_raises(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("GATEWAY_UPSTREAM_HOST", raising=False)
|
||||
config_file = tmp_path / "gateway.yaml"
|
||||
config_file.write_text(
|
||||
"""
|
||||
upstreams:
|
||||
demo:
|
||||
base_url: http://${GATEWAY_UPSTREAM_HOST}/
|
||||
routes:
|
||||
- path: /api/v1/demo
|
||||
methods: [GET]
|
||||
upstream: demo
|
||||
""".strip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Missing required environment variable"):
|
||||
load_gateway_config(config_file)
|
||||
|
||||
|
||||
def test_sample_gateway_config_matches_companion_backend_route_set() -> None:
|
||||
config = load_gateway_config(Path("config/gateway.yaml"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user