feat(deploy-control): 添加直接IP绑定功能支持
新增ipaddress模块导入以支持IP地址处理, 添加DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP环境变量配置, 实现IP地址验证、直接URL构建和端口分配功能, 当基础域名是IP地址时自动使用直接绑定模式, 支持IPv4和IPv6地址格式并添加相应参数传递
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@ -56,6 +57,7 @@ PUBLIC_SCHEME = os.environ.get("DEPLOY_PUBLIC_SCHEME", "http").strip() or "http"
|
||||
PUBLIC_BASE_DOMAIN = os.environ.get("DEPLOY_PUBLIC_BASE_DOMAIN", "localhost").strip()
|
||||
PUBLIC_HOST_TEMPLATE = os.environ.get("DEPLOY_PUBLIC_HOST_TEMPLATE", "{slug}.{base_domain}").strip()
|
||||
PUBLIC_PORT = int(os.environ.get("DEPLOY_PUBLIC_PORT", "8088").strip() or "8088")
|
||||
DIRECT_PUBLIC_HOST_BIND_IP = os.environ.get("DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP", "0.0.0.0").strip() or "0.0.0.0"
|
||||
AUTO_START_PROXY = os.environ.get("DEPLOY_AUTO_START_PROXY", "1").strip() not in {"0", "false", "False"}
|
||||
HEALTH_TIMEOUT_SECONDS = float(os.environ.get("DEPLOY_HEALTH_TIMEOUT_SECONDS", "60").strip() or "60")
|
||||
HEALTH_INTERVAL_SECONDS = float(os.environ.get("DEPLOY_HEALTH_INTERVAL_SECONDS", "1").strip() or "1")
|
||||
@ -195,6 +197,39 @@ def build_public_url(host: str) -> str:
|
||||
return f"{PUBLIC_SCHEME}://{netloc}"
|
||||
|
||||
|
||||
def public_base_domain_ip() -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
||||
value = PUBLIC_BASE_DOMAIN.strip().strip("[]")
|
||||
try:
|
||||
return ipaddress.ip_address(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def build_direct_public_url(host: ipaddress.IPv4Address | ipaddress.IPv6Address, host_port: int) -> str:
|
||||
host_value = f"[{host}]" if host.version == 6 else str(host)
|
||||
return f"http://{host_value}:{host_port}"
|
||||
|
||||
|
||||
def pick_instance_host_port(instance_id: str) -> int:
|
||||
args = [
|
||||
str(REGISTRY_TOOL),
|
||||
"--registry",
|
||||
str(REGISTRY_PATH),
|
||||
"next-port",
|
||||
"--start",
|
||||
"20000",
|
||||
"--end",
|
||||
"29999",
|
||||
]
|
||||
if instance_id:
|
||||
args.extend(["--exclude-instance-id", instance_id])
|
||||
output = run_command(args)
|
||||
try:
|
||||
return int(output.strip())
|
||||
except ValueError as exc:
|
||||
raise ApiError(HTTPStatus.BAD_GATEWAY, f"invalid registry port response: {output}") from exc
|
||||
|
||||
|
||||
def build_internal_api_base_url(record: dict[str, Any]) -> str:
|
||||
container_name = str(record.get("container_name", "") or "").strip()
|
||||
if container_name:
|
||||
@ -247,7 +282,13 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if existing is None:
|
||||
ensure_network()
|
||||
public_host = build_public_host(slug=slug, instance_id=instance_id, username=username)
|
||||
public_url = build_public_url(public_host)
|
||||
direct_public_host = public_base_domain_ip()
|
||||
host_port: int | None = None
|
||||
if direct_public_host is not None:
|
||||
host_port = pick_instance_host_port(instance_id)
|
||||
public_url = build_direct_public_url(direct_public_host, host_port)
|
||||
else:
|
||||
public_url = build_public_url(public_host)
|
||||
authz_base_url = str(payload.get("authz_base_url", "") or DEFAULT_AUTHZ_BASE_URL).strip()
|
||||
authz_outlook_mcp_url = str(
|
||||
payload.get("authz_outlook_mcp_url", "") or DEFAULT_AUTHZ_OUTLOOK_MCP_URL
|
||||
@ -279,6 +320,9 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"--network",
|
||||
INSTANCE_NETWORK_NAME,
|
||||
]
|
||||
if host_port is not None:
|
||||
command.extend(["--host-port", str(host_port)])
|
||||
command.extend(["--host-bind-ip", DIRECT_PUBLIC_HOST_BIND_IP])
|
||||
if authz_base_url:
|
||||
command.extend(["--authz-base-url", authz_base_url])
|
||||
if DEFAULT_AUTHZ_INTERNAL_TOKEN:
|
||||
|
||||
91
deploy-control/tests/test_public_url.py
Normal file
91
deploy-control/tests/test_public_url.py
Normal file
@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
SERVER_PATH = Path(__file__).resolve().parents[1] / "server.py"
|
||||
|
||||
|
||||
def _load_server_module():
|
||||
spec = importlib.util.spec_from_file_location("deploy_control_server_public_url_tests", SERVER_PATH)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_create_instance_uses_direct_host_port_url_when_base_domain_is_ip(monkeypatch) -> None:
|
||||
server = _load_server_module()
|
||||
commands: list[list[str]] = []
|
||||
record: dict[str, Any] = {
|
||||
"instance_id": "urldebug",
|
||||
"container_name": "app-instance-urldebug",
|
||||
"host_port": 20005,
|
||||
"public_url": "http://172.19.207.40:20005",
|
||||
}
|
||||
lookups = iter([None, None, record])
|
||||
|
||||
monkeypatch.setattr(server, "PUBLIC_BASE_DOMAIN", "172.19.207.40")
|
||||
monkeypatch.setattr(server, "PUBLIC_PORT", 8088)
|
||||
monkeypatch.setattr(server, "get_registry_record", lambda **_kwargs: next(lookups))
|
||||
monkeypatch.setattr(server, "ensure_network", lambda: None)
|
||||
monkeypatch.setattr(server, "ensure_proxy", lambda: None)
|
||||
monkeypatch.setattr(server, "wait_for_backend", lambda _record: None)
|
||||
monkeypatch.setattr(server, "pick_instance_host_port", lambda _instance_id: 20005)
|
||||
|
||||
def capture_command(args: list[str], **_kwargs: Any) -> str:
|
||||
commands.append(args)
|
||||
return ""
|
||||
|
||||
monkeypatch.setattr(server, "run_command", capture_command)
|
||||
|
||||
server.create_or_get_instance({
|
||||
"username": "urldebug",
|
||||
"password": "secret",
|
||||
"instance_id": "urldebug",
|
||||
})
|
||||
|
||||
create_command = commands[0]
|
||||
assert create_command[create_command.index("--host-port") + 1] == "20005"
|
||||
assert create_command[create_command.index("--host-bind-ip") + 1] == "0.0.0.0"
|
||||
assert create_command[create_command.index("--public-url") + 1] == "http://172.19.207.40:20005"
|
||||
assert create_command[create_command.index("--instance-host") + 1] == "urldebug.172.19.207.40"
|
||||
|
||||
|
||||
def test_create_instance_keeps_router_url_when_base_domain_is_dns(monkeypatch) -> None:
|
||||
server = _load_server_module()
|
||||
commands: list[list[str]] = []
|
||||
record: dict[str, Any] = {
|
||||
"instance_id": "urldebug",
|
||||
"container_name": "app-instance-urldebug",
|
||||
"host_port": 20005,
|
||||
"public_url": "https://urldebug.apps.example.com",
|
||||
}
|
||||
lookups = iter([None, None, record])
|
||||
|
||||
monkeypatch.setattr(server, "PUBLIC_SCHEME", "https")
|
||||
monkeypatch.setattr(server, "PUBLIC_BASE_DOMAIN", "apps.example.com")
|
||||
monkeypatch.setattr(server, "PUBLIC_PORT", 443)
|
||||
monkeypatch.setattr(server, "get_registry_record", lambda **_kwargs: next(lookups))
|
||||
monkeypatch.setattr(server, "ensure_network", lambda: None)
|
||||
monkeypatch.setattr(server, "ensure_proxy", lambda: None)
|
||||
monkeypatch.setattr(server, "wait_for_backend", lambda _record: None)
|
||||
monkeypatch.setattr(server, "pick_instance_host_port", lambda _instance_id: 20005)
|
||||
|
||||
def capture_command(args: list[str], **_kwargs: Any) -> str:
|
||||
commands.append(args)
|
||||
return ""
|
||||
|
||||
monkeypatch.setattr(server, "run_command", capture_command)
|
||||
|
||||
server.create_or_get_instance({
|
||||
"username": "urldebug",
|
||||
"password": "secret",
|
||||
"instance_id": "urldebug",
|
||||
})
|
||||
|
||||
create_command = commands[0]
|
||||
assert "--host-port" not in create_command
|
||||
assert create_command[create_command.index("--public-url") + 1] == "https://urldebug.apps.example.com"
|
||||
Reference in New Issue
Block a user