Files
dns-sync/examples/plugin_template/__init__.py
T

209 lines
5.2 KiB
Python

"""Canonical dns-sync adapter plugin template."""
import os
from dns_sync.adapter_support import (
require_action_endpoint,
require_desired,
require_endpoint_plugin,
)
from dns_sync.adapters import (
DNS_RECORDS,
ApplyResult,
Capability,
CapabilityOperation,
DiscoveredObject,
NormalizedObject,
PlanAction,
PlanOperation,
)
from dns_sync.config import parse_endpoint_list
from .client import ExampleClient, ExampleRecord
PLUGIN_NAME = "example"
def configured_endpoints(config):
"""Return every endpoint owned by this plugin."""
return parse_endpoint_list(
PLUGIN_NAME,
os.getenv(
"EXAMPLE_ENDPOINTS",
"example.test",
),
443,
)
class Plugin:
"""dns-sync adapter for one configured endpoint."""
name = PLUGIN_NAME
capabilities = frozenset(
{
Capability(
DNS_RECORDS,
CapabilityOperation.DISCOVER,
),
Capability(
DNS_RECORDS,
CapabilityOperation.APPLY,
),
Capability(
DNS_RECORDS,
CapabilityOperation.VERIFY,
),
}
)
def __init__(self, config, endpoint):
require_endpoint_plugin(
endpoint,
self.name,
)
self.endpoint = endpoint
self.client = ExampleClient(endpoint)
def connect(self) -> None:
self.client.connect()
def discover(
self,
object_type: str,
) -> list[DiscoveredObject]:
if object_type != DNS_RECORDS:
raise RuntimeError(
f"{self.endpoint.identity} cannot discover "
f"object type {object_type!r}"
)
return [
DiscoveredObject(
object_type=DNS_RECORDS,
local_key=record.local_key,
payload={
"hostname": record.hostname,
"ip": record.ip,
"record_type": record.record_type,
},
)
for record in self.client.list_records()
]
def _native_record(
self,
object: NormalizedObject,
) -> ExampleRecord:
if object.object_type != DNS_RECORDS:
raise RuntimeError(
f"{self.endpoint.identity} cannot handle "
f"object type {object.object_type!r}"
)
payload = object.payload
hostname = str(payload.get("hostname") or "")
if hostname != object.logical_key:
raise RuntimeError(
"DNS object hostname does not match its logical key"
)
record_type = str(
payload.get("record_type") or ""
).upper()
if record_type != "A":
raise RuntimeError(
f"Example plugin does not support "
f"{record_type!r} records"
)
if payload.get("wildcard") is True:
raise RuntimeError(
"Example plugin does not support wildcard records"
)
return ExampleRecord(
local_key=object.local_key or "",
hostname=hostname,
ip=str(payload.get("ip") or ""),
record_type=record_type,
)
def apply(
self,
action: PlanAction,
) -> ApplyResult | None:
require_action_endpoint(
action,
self.endpoint,
)
if action.operation in {
PlanOperation.NOOP,
PlanOperation.SKIP,
}:
return None
desired = require_desired(
action,
context="Example apply action",
)
record = self._native_record(desired)
if action.operation == PlanOperation.CREATE:
detail = self.client.add_record(record)
elif action.operation == PlanOperation.UPDATE:
detail = self.client.update_record(record)
else:
raise RuntimeError(
f"Example adapter does not support "
f"{action.operation.value} actions"
)
records = self.client.list_records()
applied = next(
(
candidate
for candidate in records
if candidate.hostname.lower()
== record.hostname.lower()
),
None,
)
if applied is None:
raise RuntimeError(
"Example mutation succeeded but the object "
"could not be rediscovered"
)
normalized = NormalizedObject(
object_type=desired.object_type,
logical_key=desired.logical_key,
local_key=applied.local_key,
payload=dict(desired.payload),
references=desired.references,
)
return ApplyResult(
object=normalized,
detail=detail,
)
def verify(
self,
object: NormalizedObject,
) -> bool:
return self.client.verify_record(
self._native_record(object)
)
# Add retire(self, request) only when this plugin supports safe
# deletion/replacement retirement.