Extract shared adapter validation helpers
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
from .adapters import Endpoint, NormalizedObject, PlanAction
|
||||||
|
|
||||||
|
|
||||||
|
def require_endpoint_plugin(
|
||||||
|
endpoint: Endpoint,
|
||||||
|
plugin_name: str,
|
||||||
|
) -> None:
|
||||||
|
expected = plugin_name.strip().lower()
|
||||||
|
|
||||||
|
if not expected:
|
||||||
|
raise ValueError("Adapter plugin name cannot be empty")
|
||||||
|
|
||||||
|
if endpoint.plugin != expected:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Adapter {expected!r} cannot use endpoint "
|
||||||
|
f"{endpoint.identity!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_action_endpoint(
|
||||||
|
action: PlanAction,
|
||||||
|
endpoint: Endpoint,
|
||||||
|
) -> None:
|
||||||
|
if action.endpoint_id != endpoint.identity:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Plan action for {action.endpoint_id!r} does not belong "
|
||||||
|
f"to adapter endpoint {endpoint.identity!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def require_desired(
|
||||||
|
action: PlanAction,
|
||||||
|
*,
|
||||||
|
context: str = "Adapter apply",
|
||||||
|
) -> NormalizedObject:
|
||||||
|
if action.desired is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{context} requires desired state"
|
||||||
|
)
|
||||||
|
|
||||||
|
return action.desired
|
||||||
@@ -13,6 +13,11 @@ from ...adapters import (
|
|||||||
PlanOperation,
|
PlanOperation,
|
||||||
RetireRequest,
|
RetireRequest,
|
||||||
)
|
)
|
||||||
|
from ...adapter_support import (
|
||||||
|
require_action_endpoint,
|
||||||
|
require_desired,
|
||||||
|
require_endpoint_plugin,
|
||||||
|
)
|
||||||
from ...certificates.material import CertificateMaterial
|
from ...certificates.material import CertificateMaterial
|
||||||
from ...npm_client import NpmClient
|
from ...npm_client import NpmClient
|
||||||
from ...object_normalizer import normalize_discovered_object
|
from ...object_normalizer import normalize_discovered_object
|
||||||
@@ -67,6 +72,7 @@ class Plugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config, endpoint):
|
def __init__(self, config, endpoint):
|
||||||
|
require_endpoint_plugin(endpoint, self.name)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.client = NpmClient(config, endpoint)
|
self.client = NpmClient(config, endpoint)
|
||||||
@@ -593,17 +599,15 @@ class Plugin:
|
|||||||
self,
|
self,
|
||||||
action: PlanAction,
|
action: PlanAction,
|
||||||
) -> ApplyResult | None:
|
) -> ApplyResult | None:
|
||||||
desired = action.desired
|
desired = require_desired(
|
||||||
|
action,
|
||||||
|
context="NPM apply",
|
||||||
|
)
|
||||||
|
|
||||||
if desired is None:
|
require_action_endpoint(
|
||||||
raise RuntimeError(
|
action,
|
||||||
"NPM apply requires desired state"
|
self.endpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
if action.endpoint_id != self.endpoint.identity:
|
|
||||||
raise RuntimeError(
|
|
||||||
"NPM apply action endpoint does not match adapter"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
desired.object_type == CERTIFICATES
|
desired.object_type == CERTIFICATES
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ from dns_sync.adapters import (
|
|||||||
PlanAction,
|
PlanAction,
|
||||||
PlanOperation,
|
PlanOperation,
|
||||||
)
|
)
|
||||||
|
from dns_sync.adapter_support import (
|
||||||
|
require_action_endpoint,
|
||||||
|
require_desired,
|
||||||
|
require_endpoint_plugin,
|
||||||
|
)
|
||||||
from dns_sync.models import DesiredRecord
|
from dns_sync.models import DesiredRecord
|
||||||
from .provider import Plugin as VerizonProvider
|
from .provider import Plugin as VerizonProvider
|
||||||
|
|
||||||
@@ -32,10 +37,7 @@ class Plugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config, endpoint):
|
def __init__(self, config, endpoint):
|
||||||
if endpoint.plugin != self.name:
|
require_endpoint_plugin(endpoint, self.name)
|
||||||
raise RuntimeError(
|
|
||||||
"Verizon adapter requires a verizon endpoint"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
@@ -121,10 +123,10 @@ class Plugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def apply(self, action: PlanAction) -> ApplyResult | None:
|
def apply(self, action: PlanAction) -> ApplyResult | None:
|
||||||
if action.endpoint_id != self.endpoint.identity:
|
require_action_endpoint(
|
||||||
raise RuntimeError(
|
action,
|
||||||
"Plan action belongs to a different endpoint"
|
self.endpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
if action.operation in {
|
if action.operation in {
|
||||||
PlanOperation.NOOP,
|
PlanOperation.NOOP,
|
||||||
@@ -132,12 +134,12 @@ class Plugin:
|
|||||||
}:
|
}:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if action.desired is None:
|
desired = require_desired(
|
||||||
raise RuntimeError(
|
action,
|
||||||
"Verizon apply action requires desired state"
|
context="Verizon apply action",
|
||||||
)
|
)
|
||||||
|
|
||||||
record = self._desired_record(action.desired)
|
record = self._desired_record(desired)
|
||||||
|
|
||||||
if action.operation == PlanOperation.CREATE:
|
if action.operation == PlanOperation.CREATE:
|
||||||
detail = self.provider.add_record(record)
|
detail = self.provider.add_record(record)
|
||||||
@@ -168,11 +170,11 @@ class Plugin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
applied = NormalizedObject(
|
applied = NormalizedObject(
|
||||||
object_type=action.desired.object_type,
|
object_type=desired.object_type,
|
||||||
logical_key=action.desired.logical_key,
|
logical_key=desired.logical_key,
|
||||||
local_key=str(slot),
|
local_key=str(slot),
|
||||||
payload=dict(action.desired.payload),
|
payload=dict(desired.payload),
|
||||||
references=action.desired.references,
|
references=desired.references,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ApplyResult(
|
return ApplyResult(
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from dns_sync.adapter_support import (
|
||||||
|
require_action_endpoint,
|
||||||
|
require_desired,
|
||||||
|
require_endpoint_plugin,
|
||||||
|
)
|
||||||
|
from dns_sync.adapters import (
|
||||||
|
Endpoint,
|
||||||
|
NormalizedObject,
|
||||||
|
PlanAction,
|
||||||
|
PlanOperation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterSupportTests(unittest.TestCase):
|
||||||
|
def test_endpoint_plugin_accepts_matching_plugin(self):
|
||||||
|
endpoint = Endpoint("npm", "node.example:81")
|
||||||
|
|
||||||
|
require_endpoint_plugin(endpoint, "npm")
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint_plugin_rejects_wrong_plugin(self):
|
||||||
|
endpoint = Endpoint("npm", "node.example:81")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"cannot use endpoint",
|
||||||
|
):
|
||||||
|
require_endpoint_plugin(
|
||||||
|
endpoint,
|
||||||
|
"verizon",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint_plugin_rejects_empty_expected_name(self):
|
||||||
|
endpoint = Endpoint("npm", "node.example:81")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ValueError,
|
||||||
|
"cannot be empty",
|
||||||
|
):
|
||||||
|
require_endpoint_plugin(endpoint, " ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_action_endpoint_accepts_matching_endpoint(self):
|
||||||
|
endpoint = Endpoint("npm", "node.example:81")
|
||||||
|
action = PlanAction(
|
||||||
|
endpoint_id=endpoint.identity,
|
||||||
|
operation=PlanOperation.CREATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
require_action_endpoint(action, endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
def test_action_endpoint_rejects_other_endpoint(self):
|
||||||
|
endpoint = Endpoint("npm", "node.example:81")
|
||||||
|
action = PlanAction(
|
||||||
|
endpoint_id="npm://other.example:81",
|
||||||
|
operation=PlanOperation.CREATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"does not belong",
|
||||||
|
):
|
||||||
|
require_action_endpoint(action, endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
def test_require_desired_returns_desired_object(self):
|
||||||
|
desired = NormalizedObject(
|
||||||
|
object_type="example",
|
||||||
|
logical_key="object",
|
||||||
|
payload={},
|
||||||
|
)
|
||||||
|
|
||||||
|
action = PlanAction(
|
||||||
|
endpoint_id="example://endpoint",
|
||||||
|
operation=PlanOperation.CREATE,
|
||||||
|
desired=desired,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(
|
||||||
|
require_desired(action),
|
||||||
|
desired,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_require_desired_fails_closed(self):
|
||||||
|
action = PlanAction(
|
||||||
|
endpoint_id="example://endpoint",
|
||||||
|
operation=PlanOperation.CREATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"Test mutation requires desired state",
|
||||||
|
):
|
||||||
|
require_desired(
|
||||||
|
action,
|
||||||
|
context="Test mutation",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -6,6 +6,7 @@ from dns_sync.adapters import (
|
|||||||
CERTIFICATES,
|
CERTIFICATES,
|
||||||
PROXY_HOSTS,
|
PROXY_HOSTS,
|
||||||
Capability,
|
Capability,
|
||||||
|
Endpoint,
|
||||||
CapabilityOperation,
|
CapabilityOperation,
|
||||||
NormalizedObject,
|
NormalizedObject,
|
||||||
ObjectReference,
|
ObjectReference,
|
||||||
@@ -94,6 +95,29 @@ class NpmAdapterTests(unittest.TestCase):
|
|||||||
self.assertIs(adapters[0].client, first_client)
|
self.assertIs(adapters[0].client, first_client)
|
||||||
self.assertIs(adapters[1].client, second_client)
|
self.assertIs(adapters[1].client, second_client)
|
||||||
|
|
||||||
|
def test_adapter_rejects_endpoint_for_other_plugin(self):
|
||||||
|
config = self.config()
|
||||||
|
endpoint = Endpoint(
|
||||||
|
"verizon",
|
||||||
|
"192.168.1.1:443",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"dns_sync.plugins.adapters.npm.NpmClient"
|
||||||
|
) as client_class:
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"cannot use endpoint",
|
||||||
|
):
|
||||||
|
load_adapter(
|
||||||
|
"npm",
|
||||||
|
config,
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
client_class.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_capabilities_cover_discovery_apply_and_verify(self):
|
def test_capabilities_cover_discovery_apply_and_verify(self):
|
||||||
adapter = self.adapter(Mock())
|
adapter = self.adapter(Mock())
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
|
|||||||
from dns_sync.adapters import (
|
from dns_sync.adapters import (
|
||||||
DNS_RECORDS,
|
DNS_RECORDS,
|
||||||
Capability,
|
Capability,
|
||||||
|
Endpoint,
|
||||||
CapabilityOperation,
|
CapabilityOperation,
|
||||||
NormalizedObject,
|
NormalizedObject,
|
||||||
PlanAction,
|
PlanAction,
|
||||||
@@ -79,6 +80,32 @@ class VerizonAdapterTests(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_adapter_rejects_endpoint_for_other_plugin(self):
|
||||||
|
config = self.config()
|
||||||
|
endpoint = Endpoint(
|
||||||
|
"npm",
|
||||||
|
"192.168.1.3:81",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"ENABLED_PLUGINS": "npm,verizon",
|
||||||
|
"DNS_SYNC_PLUGIN_PATH": "external_plugins",
|
||||||
|
},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RuntimeError,
|
||||||
|
"cannot use endpoint",
|
||||||
|
):
|
||||||
|
load_adapter(
|
||||||
|
"verizon",
|
||||||
|
config,
|
||||||
|
endpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_capabilities_cover_dns_discovery_apply_and_verify(self):
|
def test_capabilities_cover_dns_discovery_apply_and_verify(self):
|
||||||
adapter = self.adapter(Mock())
|
adapter = self.adapter(Mock())
|
||||||
|
|
||||||
@@ -259,7 +286,7 @@ class VerizonAdapterTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(RuntimeError, "cannot discover"):
|
with self.assertRaisesRegex(RuntimeError, "cannot discover"):
|
||||||
adapter.discover("certificates")
|
adapter.discover("certificates")
|
||||||
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "different endpoint"):
|
with self.assertRaisesRegex(RuntimeError, "does not belong"):
|
||||||
adapter.apply(
|
adapter.apply(
|
||||||
PlanAction(
|
PlanAction(
|
||||||
endpoint_id="verizon://different:443",
|
endpoint_id="verizon://different:443",
|
||||||
|
|||||||
Reference in New Issue
Block a user