"""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.