mirror of
https://github.com/furyhawk/api_gateway.git
synced 2026-07-21 02:06:50 +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:
|
3. Choose a gateway profile:
|
||||||
- `config/gateway.yaml` targets a locally running companion backend on `127.0.0.1:8068`.
|
- `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:
|
4. Run the gateway with the managed environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -215,6 +215,7 @@ make stop
|
|||||||
- `CONTAINER_ENGINE=docker` (or `podman`)
|
- `CONTAINER_ENGINE=docker` (or `podman`)
|
||||||
- `PORT=8067`
|
- `PORT=8067`
|
||||||
- `GATEWAY_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_NAME=openapi-api-gateway`
|
||||||
- `IMAGE_TAG=latest`
|
- `IMAGE_TAG=latest`
|
||||||
- `ENV_FILE=.env`
|
- `ENV_FILE=.env`
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ settings:
|
|||||||
|
|
||||||
upstreams:
|
upstreams:
|
||||||
lta_datamall:
|
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
|
port: 8068
|
||||||
timeout_seconds: 20
|
timeout_seconds: 20
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
@@ -87,6 +90,32 @@ class GatewayConfig(BaseModel):
|
|||||||
routes: list[RouteConfig]
|
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:
|
def load_gateway_config(config_path: str | Path) -> GatewayConfig:
|
||||||
path = Path(config_path)
|
path = Path(config_path)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
@@ -96,6 +125,8 @@ def load_gateway_config(config_path: str | Path) -> GatewayConfig:
|
|||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
raise ValueError("Gateway config must be a YAML object")
|
raise ValueError("Gateway config must be a YAML object")
|
||||||
|
|
||||||
|
raw = _resolve_env_placeholders(raw)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = GatewayConfig.model_validate(raw)
|
config = GatewayConfig.model_validate(raw)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
|
|||||||
@@ -74,6 +74,68 @@ routes:
|
|||||||
load_gateway_config(config_file)
|
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:
|
def test_sample_gateway_config_matches_companion_backend_route_set() -> None:
|
||||||
config = load_gateway_config(Path("config/gateway.yaml"))
|
config = load_gateway_config(Path("config/gateway.yaml"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user