Move NPM configuration and client behind plugin boundary
This commit is contained in:
+12
-58
@@ -79,15 +79,6 @@ def parse_endpoint_list(
|
|||||||
return tuple(endpoints)
|
return tuple(endpoints)
|
||||||
|
|
||||||
|
|
||||||
def endpoint_host(endpoint: Endpoint) -> str:
|
|
||||||
parsed = urlsplit(f"//{endpoint.address}")
|
|
||||||
if not parsed.hostname:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Invalid endpoint address: {endpoint.address!r}"
|
|
||||||
)
|
|
||||||
return parsed.hostname
|
|
||||||
|
|
||||||
|
|
||||||
def env_bool(name: str, default: bool) -> bool:
|
def env_bool(name: str, default: bool) -> bool:
|
||||||
value = os.getenv(name)
|
value = os.getenv(name)
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -97,64 +88,32 @@ def env_bool(name: str, default: bool) -> bool:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
npm_endpoints: tuple[Endpoint, ...]
|
|
||||||
proxy_ip: str
|
|
||||||
proxy_api_port: int
|
|
||||||
proxy_api_scheme: str
|
|
||||||
target_ip: str
|
target_ip: str
|
||||||
proxy_verify_tls: bool
|
|
||||||
domain_matching: bool
|
domain_matching: bool
|
||||||
domain: str | None
|
domain: str | None
|
||||||
include_disabled: bool = False
|
include_disabled: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls, include_disabled: bool = False) -> "AppConfig":
|
def from_env(cls, include_disabled: bool = False) -> "AppConfig":
|
||||||
try:
|
|
||||||
port = int(os.getenv("PROXY_API_PORT", "81"))
|
|
||||||
except ValueError as exc:
|
|
||||||
raise RuntimeError("PROXY_API_PORT must be an integer") from exc
|
|
||||||
if not 1 <= port <= 65535:
|
|
||||||
raise RuntimeError("PROXY_API_PORT must be between 1 and 65535")
|
|
||||||
|
|
||||||
scheme = os.getenv("PROXY_API_SCHEME", "http").strip().lower()
|
|
||||||
if scheme not in {"http", "https"}:
|
|
||||||
raise RuntimeError("PROXY_API_SCHEME must be http or https")
|
|
||||||
|
|
||||||
raw_proxy_ip = os.getenv("PROXY_IP")
|
|
||||||
raw_npm_endpoints = os.getenv("NPM_ENDPOINTS")
|
|
||||||
|
|
||||||
if raw_npm_endpoints is None:
|
|
||||||
if not raw_proxy_ip:
|
|
||||||
raise RuntimeError(
|
|
||||||
"NPM_ENDPOINTS or legacy PROXY_IP is required"
|
|
||||||
)
|
|
||||||
proxy_ip = normalize_ipv4(raw_proxy_ip, "PROXY_IP")
|
|
||||||
npm_endpoints = parse_endpoint_list("npm", proxy_ip, port)
|
|
||||||
else:
|
|
||||||
npm_endpoints = parse_endpoint_list(
|
|
||||||
"npm",
|
|
||||||
raw_npm_endpoints,
|
|
||||||
port,
|
|
||||||
)
|
|
||||||
if raw_proxy_ip:
|
|
||||||
proxy_ip = normalize_ipv4(raw_proxy_ip, "PROXY_IP")
|
|
||||||
else:
|
|
||||||
proxy_ip = endpoint_host(npm_endpoints[0])
|
|
||||||
|
|
||||||
raw_target_ip = os.getenv("DNS_TARGET_IP")
|
raw_target_ip = os.getenv("DNS_TARGET_IP")
|
||||||
|
raw_proxy_ip = os.getenv("PROXY_IP")
|
||||||
|
|
||||||
if raw_target_ip:
|
if raw_target_ip:
|
||||||
target_ip = normalize_ipv4(raw_target_ip, "DNS_TARGET_IP")
|
|
||||||
elif raw_proxy_ip:
|
|
||||||
target_ip = proxy_ip
|
|
||||||
elif len(npm_endpoints) == 1:
|
|
||||||
target_ip = normalize_ipv4(
|
target_ip = normalize_ipv4(
|
||||||
endpoint_host(npm_endpoints[0]),
|
raw_target_ip,
|
||||||
"DNS_TARGET_IP",
|
"DNS_TARGET_IP",
|
||||||
)
|
)
|
||||||
|
elif raw_proxy_ip:
|
||||||
|
# Deprecated compatibility fallback. Core no longer treats
|
||||||
|
# PROXY_IP as an NPM endpoint.
|
||||||
|
target_ip = normalize_ipv4(
|
||||||
|
raw_proxy_ip,
|
||||||
|
"PROXY_IP",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"DNS_TARGET_IP is required when multiple NPM_ENDPOINTS "
|
"DNS_TARGET_IP is required "
|
||||||
"are configured without legacy PROXY_IP"
|
"(legacy PROXY_IP is also accepted)"
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_matching = env_bool("DOMAIN_MATCHING", False)
|
domain_matching = env_bool("DOMAIN_MATCHING", False)
|
||||||
@@ -173,12 +132,7 @@ class AppConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
npm_endpoints=npm_endpoints,
|
|
||||||
proxy_ip=proxy_ip,
|
|
||||||
proxy_api_port=port,
|
|
||||||
proxy_api_scheme=scheme,
|
|
||||||
target_ip=target_ip,
|
target_ip=target_ip,
|
||||||
proxy_verify_tls=env_bool("PROXY_VERIFY_TLS", True),
|
|
||||||
domain_matching=domain_matching,
|
domain_matching=domain_matching,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
include_disabled=include_disabled,
|
include_disabled=include_disabled,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ...adapters import (
|
from ....adapters import (
|
||||||
CERTIFICATES,
|
CERTIFICATES,
|
||||||
PROXY_HOSTS,
|
PROXY_HOSTS,
|
||||||
ApplyResult,
|
ApplyResult,
|
||||||
@@ -13,18 +14,49 @@ from ...adapters import (
|
|||||||
PlanOperation,
|
PlanOperation,
|
||||||
RetireRequest,
|
RetireRequest,
|
||||||
)
|
)
|
||||||
from ...adapter_support import (
|
from ....adapter_support import (
|
||||||
require_action_endpoint,
|
require_action_endpoint,
|
||||||
require_desired,
|
require_desired,
|
||||||
require_endpoint_plugin,
|
require_endpoint_plugin,
|
||||||
)
|
)
|
||||||
from ...certificates.material import CertificateMaterial
|
from ....certificates.material import CertificateMaterial
|
||||||
from ...npm_client import NpmClient
|
from ....config import parse_endpoint_list
|
||||||
from ...object_normalizer import normalize_discovered_object
|
from ....normalizer import normalize_ipv4
|
||||||
|
from .client import NpmClient
|
||||||
|
from ....object_normalizer import normalize_discovered_object
|
||||||
|
|
||||||
|
|
||||||
def configured_endpoints(config):
|
def configured_endpoints(config):
|
||||||
return config.npm_endpoints
|
try:
|
||||||
|
port = int(os.getenv("PROXY_API_PORT", "81"))
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
"PROXY_API_PORT must be an integer"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not 1 <= port <= 65535:
|
||||||
|
raise RuntimeError(
|
||||||
|
"PROXY_API_PORT must be between 1 and 65535"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_endpoints = os.getenv("NPM_ENDPOINTS")
|
||||||
|
if raw_endpoints is None:
|
||||||
|
raw_proxy_ip = os.getenv("PROXY_IP")
|
||||||
|
if not raw_proxy_ip:
|
||||||
|
raise RuntimeError(
|
||||||
|
"NPM_ENDPOINTS or legacy PROXY_IP is required"
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_endpoints = normalize_ipv4(
|
||||||
|
raw_proxy_ip,
|
||||||
|
"PROXY_IP",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parse_endpoint_list(
|
||||||
|
"npm",
|
||||||
|
raw_endpoints,
|
||||||
|
port,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_PROXY_HOST_FIELDS = (
|
_PROXY_HOST_FIELDS = (
|
||||||
@@ -77,9 +109,9 @@ class Plugin:
|
|||||||
|
|
||||||
def __init__(self, config, endpoint):
|
def __init__(self, config, endpoint):
|
||||||
require_endpoint_plugin(endpoint, self.name)
|
require_endpoint_plugin(endpoint, self.name)
|
||||||
self.config = config
|
self.endpoints = configured_endpoints(config)
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.client = NpmClient(config, endpoint)
|
self.client = NpmClient(endpoint)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _local_key(row: dict[str, Any], label: str) -> str:
|
def _local_key(row: dict[str, Any], label: str) -> str:
|
||||||
@@ -246,16 +278,13 @@ class Plugin:
|
|||||||
source_errors: list[str] = []
|
source_errors: list[str] = []
|
||||||
|
|
||||||
for endpoint in sorted(
|
for endpoint in sorted(
|
||||||
self.config.npm_endpoints,
|
self.endpoints,
|
||||||
key=lambda item: item.identity,
|
key=lambda item: item.identity,
|
||||||
):
|
):
|
||||||
if endpoint.identity == self.endpoint.identity:
|
if endpoint.identity == self.endpoint.identity:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
client = NpmClient(
|
client = NpmClient(endpoint)
|
||||||
self.config,
|
|
||||||
endpoint,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rows = client.certificates()
|
rows = client.certificates()
|
||||||
@@ -7,8 +7,9 @@ from zipfile import BadZipFile, ZipFile
|
|||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
from .adapters import Endpoint
|
from ....adapters import Endpoint
|
||||||
from .certificates.material import CertificateMaterial
|
from ....certificates.material import CertificateMaterial
|
||||||
|
from ....config import env_bool
|
||||||
|
|
||||||
|
|
||||||
class NpmClient:
|
class NpmClient:
|
||||||
@@ -16,11 +17,9 @@ class NpmClient:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config,
|
endpoint: Endpoint,
|
||||||
endpoint: Endpoint | None = None,
|
|
||||||
):
|
):
|
||||||
self.config = config
|
self.endpoint = endpoint
|
||||||
self.endpoint = endpoint or config.npm_endpoints[0]
|
|
||||||
|
|
||||||
if self.endpoint.plugin != "npm":
|
if self.endpoint.plugin != "npm":
|
||||||
raise RuntimeError("NpmClient requires an npm endpoint")
|
raise RuntimeError("NpmClient requires an npm endpoint")
|
||||||
@@ -34,10 +33,25 @@ class NpmClient:
|
|||||||
if not self.secret:
|
if not self.secret:
|
||||||
raise RuntimeError("NPM_SECRET is required")
|
raise RuntimeError("NPM_SECRET is required")
|
||||||
|
|
||||||
|
self.api_scheme = os.getenv(
|
||||||
|
"PROXY_API_SCHEME",
|
||||||
|
"http",
|
||||||
|
).strip().lower()
|
||||||
|
|
||||||
|
if self.api_scheme not in {"http", "https"}:
|
||||||
|
raise RuntimeError(
|
||||||
|
"PROXY_API_SCHEME must be http or https"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.verify_tls = env_bool(
|
||||||
|
"PROXY_VERIFY_TLS",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.token: str | None = None
|
self.token: str | None = None
|
||||||
|
|
||||||
if not config.proxy_verify_tls:
|
if not self.verify_tls:
|
||||||
urllib3.disable_warnings(
|
urllib3.disable_warnings(
|
||||||
urllib3.exceptions.InsecureRequestWarning
|
urllib3.exceptions.InsecureRequestWarning
|
||||||
)
|
)
|
||||||
@@ -45,7 +59,7 @@ class NpmClient:
|
|||||||
@property
|
@property
|
||||||
def api_url(self) -> str:
|
def api_url(self) -> str:
|
||||||
return (
|
return (
|
||||||
f"{self.config.proxy_api_scheme}://"
|
f"{self.api_scheme}://"
|
||||||
f"{self.endpoint.address}/api"
|
f"{self.endpoint.address}/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,7 +71,7 @@ class NpmClient:
|
|||||||
"secret": self.secret,
|
"secret": self.secret,
|
||||||
},
|
},
|
||||||
timeout=15,
|
timeout=15,
|
||||||
verify=self.config.proxy_verify_tls,
|
verify=self.verify_tls,
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@@ -107,7 +121,7 @@ class NpmClient:
|
|||||||
method,
|
method,
|
||||||
f"{self.api_url}{path}",
|
f"{self.api_url}{path}",
|
||||||
timeout=kwargs.pop("timeout", 15),
|
timeout=kwargs.pop("timeout", 15),
|
||||||
verify=self.config.proxy_verify_tls,
|
verify=self.verify_tls,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
+13
-33
@@ -47,28 +47,27 @@ class EndpointParsingTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppConfigEndpointTests(unittest.TestCase):
|
class AppConfigPolicyTests(unittest.TestCase):
|
||||||
def test_legacy_proxy_ip_remains_supported(self):
|
def test_explicit_dns_target_is_used(self):
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{"PROXY_IP": "192.168.1.101"},
|
{
|
||||||
|
"DNS_TARGET_IP": "192.168.1.101",
|
||||||
|
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
|
||||||
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
config = AppConfig.from_env()
|
config = AppConfig.from_env()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
tuple(endpoint.identity for endpoint in config.npm_endpoints),
|
config.target_ip,
|
||||||
("npm://192.168.1.101:81",),
|
"192.168.1.101",
|
||||||
)
|
)
|
||||||
self.assertEqual(config.target_ip, "192.168.1.101")
|
|
||||||
|
|
||||||
def test_endpoint_list_controls_api_and_proxy_ip_remains_target(self):
|
def test_legacy_proxy_ip_remains_target_fallback(self):
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": (
|
|
||||||
"192.168.1.3,192.168.1.4:8081,192.168.1.5"
|
|
||||||
),
|
|
||||||
"PROXY_IP": "192.168.1.101",
|
"PROXY_IP": "192.168.1.101",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
@@ -76,33 +75,15 @@ class AppConfigEndpointTests(unittest.TestCase):
|
|||||||
config = AppConfig.from_env()
|
config = AppConfig.from_env()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
tuple(endpoint.identity for endpoint in config.npm_endpoints),
|
config.target_ip,
|
||||||
(
|
"192.168.1.101",
|
||||||
"npm://192.168.1.3:81",
|
|
||||||
"npm://192.168.1.4:8081",
|
|
||||||
"npm://192.168.1.5:81",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.assertEqual(config.target_ip, "192.168.1.101")
|
|
||||||
|
|
||||||
def test_explicit_dns_target_supports_multiple_endpoints(self):
|
def test_dns_target_is_required_without_legacy_fallback(self):
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
|
"NPM_ENDPOINTS": "192.168.1.3",
|
||||||
"DNS_TARGET_IP": "192.168.1.101",
|
|
||||||
},
|
|
||||||
clear=True,
|
|
||||||
):
|
|
||||||
config = AppConfig.from_env()
|
|
||||||
|
|
||||||
self.assertEqual(config.target_ip, "192.168.1.101")
|
|
||||||
|
|
||||||
def test_multiple_endpoints_require_target_without_proxy_ip(self):
|
|
||||||
with patch.dict(
|
|
||||||
os.environ,
|
|
||||||
{
|
|
||||||
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
|
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
@@ -113,6 +94,5 @@ class AppConfigEndpointTests(unittest.TestCase):
|
|||||||
AppConfig.from_env()
|
AppConfig.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
+120
-112
@@ -16,73 +16,103 @@ from dns_sync.adapters import (
|
|||||||
from dns_sync.certificates.material import CertificateMaterial
|
from dns_sync.certificates.material import CertificateMaterial
|
||||||
from dns_sync.config import AppConfig
|
from dns_sync.config import AppConfig
|
||||||
from dns_sync.object_normalizer import normalize_discovered_object
|
from dns_sync.object_normalizer import normalize_discovered_object
|
||||||
from dns_sync.registry import load_adapter
|
from dns_sync.registry import (
|
||||||
|
configured_adapter_endpoints,
|
||||||
|
load_adapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NpmAdapterTests(unittest.TestCase):
|
class NpmAdapterTests(unittest.TestCase):
|
||||||
def config(self):
|
@staticmethod
|
||||||
with patch.dict(
|
def npm_env(
|
||||||
|
endpoints="192.168.1.3,192.168.1.4",
|
||||||
|
):
|
||||||
|
return patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4",
|
"NPM_ENDPOINTS": endpoints,
|
||||||
"DNS_TARGET_IP": "192.168.1.101",
|
"DNS_TARGET_IP": "192.168.1.101",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
)
|
||||||
|
|
||||||
|
def config(self):
|
||||||
|
with self.npm_env():
|
||||||
return AppConfig.from_env()
|
return AppConfig.from_env()
|
||||||
|
|
||||||
def adapter(self, client):
|
def adapter(self, client):
|
||||||
config = self.config()
|
with self.npm_env():
|
||||||
with patch(
|
config = AppConfig.from_env()
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
endpoints = configured_adapter_endpoints(
|
||||||
return_value=client,
|
|
||||||
):
|
|
||||||
return load_adapter(
|
|
||||||
"npm",
|
"npm",
|
||||||
config,
|
config,
|
||||||
config.npm_endpoints[0],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
|
return_value=client,
|
||||||
|
):
|
||||||
|
return load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoints[0],
|
||||||
|
)
|
||||||
|
|
||||||
def target_adapter_with_sources(
|
def target_adapter_with_sources(
|
||||||
self,
|
self,
|
||||||
target_client,
|
target_client,
|
||||||
source_clients,
|
source_clients,
|
||||||
):
|
):
|
||||||
config = self.config()
|
with self.npm_env():
|
||||||
|
config = AppConfig.from_env()
|
||||||
with patch(
|
endpoints = configured_adapter_endpoints(
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
|
||||||
return_value=target_client,
|
|
||||||
):
|
|
||||||
adapter = load_adapter(
|
|
||||||
"npm",
|
"npm",
|
||||||
config,
|
config,
|
||||||
config.npm_endpoints[1],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
|
return_value=target_client,
|
||||||
|
):
|
||||||
|
adapter = load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoints[1],
|
||||||
|
)
|
||||||
|
|
||||||
client_by_identity = {
|
client_by_identity = {
|
||||||
endpoint_id: client
|
endpoint_id: client
|
||||||
for endpoint_id, client in source_clients.items()
|
for endpoint_id, client in source_clients.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def client_factory(config, endpoint):
|
def client_factory(endpoint):
|
||||||
return client_by_identity[endpoint.identity]
|
return client_by_identity[endpoint.identity]
|
||||||
|
|
||||||
return adapter, client_factory
|
return adapter, client_factory
|
||||||
|
|
||||||
def test_registry_creates_one_adapter_per_endpoint(self):
|
def test_registry_creates_one_adapter_per_endpoint(self):
|
||||||
config = self.config()
|
|
||||||
first_client = Mock()
|
first_client = Mock()
|
||||||
second_client = Mock()
|
second_client = Mock()
|
||||||
|
|
||||||
with patch(
|
with self.npm_env():
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
config = AppConfig.from_env()
|
||||||
side_effect=[first_client, second_client],
|
endpoints = configured_adapter_endpoints(
|
||||||
) as client_class:
|
"npm",
|
||||||
adapters = [
|
config,
|
||||||
load_adapter("npm", config, endpoint)
|
)
|
||||||
for endpoint in config.npm_endpoints
|
|
||||||
]
|
with patch(
|
||||||
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
|
side_effect=[first_client, second_client],
|
||||||
|
) as client_class:
|
||||||
|
adapters = [
|
||||||
|
load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
for endpoint in endpoints
|
||||||
|
]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
tuple(adapter.endpoint.identity for adapter in adapters),
|
tuple(adapter.endpoint.identity for adapter in adapters),
|
||||||
@@ -517,25 +547,14 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
source_client.certificate_material.assert_not_called()
|
source_client.certificate_material.assert_not_called()
|
||||||
|
|
||||||
def test_certificate_material_source_rejects_conflicting_material(self):
|
def test_certificate_material_source_rejects_conflicting_material(self):
|
||||||
config = self.config()
|
with self.npm_env(
|
||||||
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
# Add a third endpoint for this ambiguity test.
|
):
|
||||||
endpoint_type = type(config.npm_endpoints[0])
|
config = AppConfig.from_env()
|
||||||
third_endpoint = endpoint_type(
|
endpoints = configured_adapter_endpoints(
|
||||||
"npm",
|
"npm",
|
||||||
"192.168.1.5:81",
|
config,
|
||||||
)
|
)
|
||||||
|
|
||||||
config = __import__(
|
|
||||||
"dataclasses"
|
|
||||||
).replace(
|
|
||||||
config,
|
|
||||||
npm_endpoints=(
|
|
||||||
config.npm_endpoints[0],
|
|
||||||
config.npm_endpoints[1],
|
|
||||||
third_endpoint,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
target_client = Mock()
|
target_client = Mock()
|
||||||
source_a = Mock()
|
source_a = Mock()
|
||||||
@@ -568,22 +587,25 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with self.npm_env(
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
return_value=target_client,
|
|
||||||
):
|
):
|
||||||
adapter = load_adapter(
|
with patch(
|
||||||
"npm",
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
config,
|
return_value=target_client,
|
||||||
config.npm_endpoints[1],
|
):
|
||||||
)
|
adapter = load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoints[1],
|
||||||
|
)
|
||||||
|
|
||||||
clients = {
|
clients = {
|
||||||
"npm://192.168.1.3:81": source_a,
|
"npm://192.168.1.3:81": source_a,
|
||||||
"npm://192.168.1.5:81": source_b,
|
"npm://192.168.1.5:81": source_b,
|
||||||
}
|
}
|
||||||
|
|
||||||
def client_factory(config, endpoint):
|
def client_factory(endpoint):
|
||||||
return clients[endpoint.identity]
|
return clients[endpoint.identity]
|
||||||
|
|
||||||
desired = NormalizedObject(
|
desired = NormalizedObject(
|
||||||
@@ -607,24 +629,14 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
adapter._certificate_material_source(desired)
|
adapter._certificate_material_source(desired)
|
||||||
|
|
||||||
def test_certificate_material_source_accepts_identical_replicas(self):
|
def test_certificate_material_source_accepts_identical_replicas(self):
|
||||||
config = self.config()
|
with self.npm_env(
|
||||||
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
endpoint_type = type(config.npm_endpoints[0])
|
):
|
||||||
third_endpoint = endpoint_type(
|
config = AppConfig.from_env()
|
||||||
"npm",
|
endpoints = configured_adapter_endpoints(
|
||||||
"192.168.1.5:81",
|
"npm",
|
||||||
)
|
config,
|
||||||
|
)
|
||||||
config = __import__(
|
|
||||||
"dataclasses"
|
|
||||||
).replace(
|
|
||||||
config,
|
|
||||||
npm_endpoints=(
|
|
||||||
config.npm_endpoints[0],
|
|
||||||
config.npm_endpoints[1],
|
|
||||||
third_endpoint,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
target_client = Mock()
|
target_client = Mock()
|
||||||
source_a = Mock()
|
source_a = Mock()
|
||||||
@@ -660,22 +672,25 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
source_a.certificate_material.return_value = common_material
|
source_a.certificate_material.return_value = common_material
|
||||||
source_b.certificate_material.return_value = common_material
|
source_b.certificate_material.return_value = common_material
|
||||||
|
|
||||||
with patch(
|
with self.npm_env(
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
return_value=target_client,
|
|
||||||
):
|
):
|
||||||
adapter = load_adapter(
|
with patch(
|
||||||
"npm",
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
config,
|
return_value=target_client,
|
||||||
config.npm_endpoints[1],
|
):
|
||||||
)
|
adapter = load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoints[1],
|
||||||
|
)
|
||||||
|
|
||||||
clients = {
|
clients = {
|
||||||
"npm://192.168.1.3:81": source_a,
|
"npm://192.168.1.3:81": source_a,
|
||||||
"npm://192.168.1.5:81": source_b,
|
"npm://192.168.1.5:81": source_b,
|
||||||
}
|
}
|
||||||
|
|
||||||
def client_factory(config, endpoint):
|
def client_factory(endpoint):
|
||||||
return clients[endpoint.identity]
|
return clients[endpoint.identity]
|
||||||
|
|
||||||
desired = NormalizedObject(
|
desired = NormalizedObject(
|
||||||
@@ -699,24 +714,14 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
self.assertEqual(resolved, common_material)
|
self.assertEqual(resolved, common_material)
|
||||||
|
|
||||||
def test_certificate_material_source_tolerates_unavailable_unrelated_source(self):
|
def test_certificate_material_source_tolerates_unavailable_unrelated_source(self):
|
||||||
config = self.config()
|
with self.npm_env(
|
||||||
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
endpoint_type = type(config.npm_endpoints[0])
|
):
|
||||||
third_endpoint = endpoint_type(
|
config = AppConfig.from_env()
|
||||||
"npm",
|
endpoints = configured_adapter_endpoints(
|
||||||
"192.168.1.5:81",
|
"npm",
|
||||||
)
|
config,
|
||||||
|
)
|
||||||
config = __import__(
|
|
||||||
"dataclasses"
|
|
||||||
).replace(
|
|
||||||
config,
|
|
||||||
npm_endpoints=(
|
|
||||||
config.npm_endpoints[0],
|
|
||||||
config.npm_endpoints[1],
|
|
||||||
third_endpoint,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
target_client = Mock()
|
target_client = Mock()
|
||||||
unavailable = Mock()
|
unavailable = Mock()
|
||||||
@@ -743,22 +748,25 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
available.certificate_material.return_value = material
|
available.certificate_material.return_value = material
|
||||||
|
|
||||||
with patch(
|
with self.npm_env(
|
||||||
"dns_sync.plugins.adapters.npm.NpmClient",
|
"192.168.1.3,192.168.1.4,192.168.1.5"
|
||||||
return_value=target_client,
|
|
||||||
):
|
):
|
||||||
adapter = load_adapter(
|
with patch(
|
||||||
"npm",
|
"dns_sync.plugins.adapters.npm.NpmClient",
|
||||||
config,
|
return_value=target_client,
|
||||||
config.npm_endpoints[1],
|
):
|
||||||
)
|
adapter = load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoints[1],
|
||||||
|
)
|
||||||
|
|
||||||
clients = {
|
clients = {
|
||||||
"npm://192.168.1.3:81": unavailable,
|
"npm://192.168.1.3:81": unavailable,
|
||||||
"npm://192.168.1.5:81": available,
|
"npm://192.168.1.5:81": available,
|
||||||
}
|
}
|
||||||
|
|
||||||
def client_factory(config, endpoint):
|
def client_factory(endpoint):
|
||||||
return clients[endpoint.identity]
|
return clients[endpoint.identity]
|
||||||
|
|
||||||
desired = NormalizedObject(
|
desired = NormalizedObject(
|
||||||
|
|||||||
+74
-27
@@ -5,48 +5,49 @@ from zipfile import ZipFile
|
|||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from dns_sync.adapters import Endpoint
|
from dns_sync.adapters import Endpoint
|
||||||
from dns_sync.config import AppConfig
|
|
||||||
from dns_sync.certificates.material import CertificateMaterial
|
from dns_sync.certificates.material import CertificateMaterial
|
||||||
from dns_sync.npm_client import NpmClient
|
from dns_sync.plugins.adapters.npm.client import NpmClient
|
||||||
|
|
||||||
|
|
||||||
class NpmClientEndpointTests(unittest.TestCase):
|
class NpmClientEndpointTests(unittest.TestCase):
|
||||||
def config(self):
|
def test_client_uses_explicit_endpoint(self):
|
||||||
return AppConfig.from_env()
|
|
||||||
|
|
||||||
def test_default_client_uses_first_configured_endpoint(self):
|
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4:8081",
|
|
||||||
"DNS_TARGET_IP": "192.168.1.101",
|
|
||||||
"NPM_IDENTITY": "test@example.com",
|
"NPM_IDENTITY": "test@example.com",
|
||||||
"NPM_SECRET": "not-a-real-secret",
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
config = self.config()
|
endpoint = Endpoint(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3:81",
|
||||||
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"dns_sync.npm_client.requests.Session.request"
|
"dns_sync.plugins.adapters.npm.client.requests.Session.request"
|
||||||
) as request:
|
) as request:
|
||||||
client = NpmClient(config)
|
client = NpmClient(endpoint)
|
||||||
|
|
||||||
request.assert_not_called()
|
request.assert_not_called()
|
||||||
self.assertEqual(client.api_url, "http://192.168.1.3:81/api")
|
self.assertEqual(
|
||||||
|
client.api_url,
|
||||||
|
"http://192.168.1.3:81/api",
|
||||||
|
)
|
||||||
|
|
||||||
def test_client_can_select_specific_configured_endpoint(self):
|
def test_client_can_select_specific_endpoint(self):
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": "192.168.1.3,192.168.1.4:8081",
|
|
||||||
"DNS_TARGET_IP": "192.168.1.101",
|
|
||||||
"NPM_IDENTITY": "test@example.com",
|
"NPM_IDENTITY": "test@example.com",
|
||||||
"NPM_SECRET": "not-a-real-secret",
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
config = self.config()
|
endpoint = Endpoint(
|
||||||
client = NpmClient(config, config.npm_endpoints[1])
|
"npm",
|
||||||
|
"192.168.1.4:8081",
|
||||||
|
)
|
||||||
|
client = NpmClient(endpoint)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
client.api_url,
|
client.api_url,
|
||||||
@@ -61,19 +62,66 @@ class NpmClientEndpointTests(unittest.TestCase):
|
|||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"PROXY_IP": "192.168.1.101",
|
|
||||||
"NPM_IDENTITY": "test@example.com",
|
"NPM_IDENTITY": "test@example.com",
|
||||||
"NPM_SECRET": "not-a-real-secret",
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
config = self.config()
|
endpoint = Endpoint(
|
||||||
endpoint = Endpoint("verizon", "192.168.1.1")
|
"verizon",
|
||||||
|
"192.168.1.1:443",
|
||||||
|
)
|
||||||
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "npm endpoint"):
|
with self.assertRaisesRegex(
|
||||||
NpmClient(config, endpoint)
|
RuntimeError,
|
||||||
|
"npm endpoint",
|
||||||
|
):
|
||||||
|
NpmClient(endpoint)
|
||||||
|
|
||||||
|
def test_client_rejects_invalid_api_scheme(self):
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"NPM_IDENTITY": "test@example.com",
|
||||||
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
|
"PROXY_API_SCHEME": "ftp",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"PROXY_API_SCHEME",
|
||||||
|
):
|
||||||
|
NpmClient(
|
||||||
|
Endpoint(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3:81",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_client_owns_tls_verification_setting(self):
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"NPM_IDENTITY": "test@example.com",
|
||||||
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
|
"PROXY_API_SCHEME": "https",
|
||||||
|
"PROXY_VERIFY_TLS": "false",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
client = NpmClient(
|
||||||
|
Endpoint(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3:81",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
client.api_url,
|
||||||
|
"https://192.168.1.3:81/api",
|
||||||
|
)
|
||||||
|
self.assertFalse(client.verify_tls)
|
||||||
|
|
||||||
|
|
||||||
class NpmClientMutationTests(unittest.TestCase):
|
class NpmClientMutationTests(unittest.TestCase):
|
||||||
@@ -81,17 +129,16 @@ class NpmClientMutationTests(unittest.TestCase):
|
|||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"NPM_ENDPOINTS": "192.168.1.3",
|
|
||||||
"DNS_TARGET_IP": "192.168.1.101",
|
|
||||||
"NPM_IDENTITY": "test@example.com",
|
"NPM_IDENTITY": "test@example.com",
|
||||||
"NPM_SECRET": "not-a-real-secret",
|
"NPM_SECRET": "not-a-real-secret",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
config = AppConfig.from_env()
|
|
||||||
return NpmClient(
|
return NpmClient(
|
||||||
config,
|
Endpoint(
|
||||||
config.npm_endpoints[0],
|
"npm",
|
||||||
|
"192.168.1.3:81",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -228,14 +228,13 @@ class PluginRegistryTests(unittest.TestCase):
|
|||||||
"npm",
|
"npm",
|
||||||
"192.168.1.3:81",
|
"192.168.1.3:81",
|
||||||
)
|
)
|
||||||
config = SimpleNamespace(
|
config = SimpleNamespace()
|
||||||
npm_endpoints=(endpoint,),
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"ENABLED_PLUGINS": "npm",
|
"ENABLED_PLUGINS": "npm",
|
||||||
|
"NPM_ENDPOINTS": "192.168.1.3",
|
||||||
},
|
},
|
||||||
clear=True,
|
clear=True,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class VerizonAdapterTests(unittest.TestCase):
|
|||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
|
"DNS_TARGET_IP": "192.168.1.101",
|
||||||
"NPM_ENDPOINTS": "192.168.1.3",
|
"NPM_ENDPOINTS": "192.168.1.3",
|
||||||
"VERIZON_ENDPOINTS": "192.168.1.1",
|
"VERIZON_ENDPOINTS": "192.168.1.1",
|
||||||
"ENABLED_PLUGINS": "npm,verizon",
|
"ENABLED_PLUGINS": "npm,verizon",
|
||||||
@@ -141,6 +142,7 @@ class VerizonAdapterTests(unittest.TestCase):
|
|||||||
with patch.dict(
|
with patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
|
"DNS_TARGET_IP": "192.168.1.101",
|
||||||
"NPM_ENDPOINTS": "192.168.1.3",
|
"NPM_ENDPOINTS": "192.168.1.3",
|
||||||
"VERIZON_ENDPOINTS": "192.168.1.1:8443",
|
"VERIZON_ENDPOINTS": "192.168.1.1:8443",
|
||||||
"ENABLED_PLUGINS": "npm,verizon",
|
"ENABLED_PLUGINS": "npm,verizon",
|
||||||
|
|||||||
Reference in New Issue
Block a user