use async_trait::async_trait;
use crate::{CloudflareClient, Error as CloudflareError};
use lmrc_ports::{
DnsProvider, DnsRecord, DnsRecordRequest, DnsRecordType, PortError, PortResult,
};
pub struct CloudflareAdapter {
client: CloudflareClient,
}
impl CloudflareAdapter {
pub fn new(client: CloudflareClient) -> Self {
Self { client }
}
pub fn from_env() -> PortResult<Self> {
let api_token = std::env::var("CLOUDFLARE_API_TOKEN").map_err(|_| {
PortError::AuthenticationFailed(
"CLOUDFLARE_API_TOKEN environment variable not set".to_string(),
)
})?;
let client = CloudflareClient::new(&api_token)
.map_err(|e| PortError::AuthenticationFailed(e.to_string()))?;
Ok(Self::new(client))
}
pub fn with_token(api_token: String) -> PortResult<Self> {
let client = CloudflareClient::new(&api_token)
.map_err(|e| PortError::AuthenticationFailed(e.to_string()))?;
Ok(Self::new(client))
}
}
fn convert_record_type(record_type: &DnsRecordType) -> crate::dns::RecordType {
match record_type {
DnsRecordType::A => crate::dns::RecordType::A,
DnsRecordType::AAAA => crate::dns::RecordType::AAAA,
DnsRecordType::CNAME => crate::dns::RecordType::CNAME,
DnsRecordType::MX => crate::dns::RecordType::MX,
DnsRecordType::TXT => crate::dns::RecordType::TXT,
DnsRecordType::NS => crate::dns::RecordType::NS,
DnsRecordType::SRV => crate::dns::RecordType::SRV,
DnsRecordType::CAA => crate::dns::RecordType::CAA,
}
}
fn convert_record_type_from_string(record_type: &str) -> DnsRecordType {
match record_type.to_uppercase().as_str() {
"A" => DnsRecordType::A,
"AAAA" => DnsRecordType::AAAA,
"CNAME" => DnsRecordType::CNAME,
"MX" => DnsRecordType::MX,
"TXT" => DnsRecordType::TXT,
"NS" => DnsRecordType::NS,
"SRV" => DnsRecordType::SRV,
"CAA" => DnsRecordType::CAA,
_ => DnsRecordType::A, }
}
fn convert_dns_record(record: crate::dns::DnsRecord, zone_id: &str) -> DnsRecord {
DnsRecord {
id: record.id,
zone_id: zone_id.to_string(),
record_type: convert_record_type_from_string(&record.record_type),
name: record.name,
content: record.content,
ttl: record.ttl,
proxied: record.proxied,
priority: record.priority,
}
}
fn convert_error(err: CloudflareError) -> PortError {
match err {
CloudflareError::Unauthorized(msg) => PortError::AuthenticationFailed(msg),
CloudflareError::NotFound(message) => PortError::NotFound {
resource_type: "DNS Record".to_string(),
resource_id: message,
},
CloudflareError::RateLimited { retry_after } => PortError::RateLimitExceeded {
retry_after_seconds: retry_after,
},
CloudflareError::InvalidInput(message) => PortError::InvalidConfiguration(message),
CloudflareError::Http(e) => PortError::NetworkError(e.to_string()),
_ => PortError::ProviderError(err.to_string()),
}
}
#[async_trait]
impl DnsProvider for CloudflareAdapter {
async fn create_record(
&self,
zone_id: &str,
request: DnsRecordRequest,
) -> PortResult<DnsRecord> {
let mut builder = self
.client
.dns()
.create_record(zone_id)
.name(&request.name)
.record_type(convert_record_type(&request.record_type))
.content(&request.content)
.proxied(request.proxied);
if let Some(ttl) = request.ttl {
builder = builder.ttl(ttl);
}
if let Some(priority) = request.priority {
builder = builder.priority(priority);
}
let record = builder.send().await.map_err(convert_error)?;
Ok(convert_dns_record(record, zone_id))
}
async fn list_records(&self, zone_id: &str) -> PortResult<Vec<DnsRecord>> {
let records = self
.client
.dns()
.list_records(zone_id)
.send()
.await
.map_err(convert_error)?;
Ok(records
.into_iter()
.map(|r| convert_dns_record(r, zone_id))
.collect())
}
async fn get_record(&self, zone_id: &str, record_id: &str) -> PortResult<DnsRecord> {
let record = self
.client
.dns()
.get_record(zone_id, record_id)
.await
.map_err(convert_error)?;
Ok(convert_dns_record(record, zone_id))
}
async fn update_record(
&self,
zone_id: &str,
record_id: &str,
request: DnsRecordRequest,
) -> PortResult<DnsRecord> {
let mut builder = self
.client
.dns()
.update_record(zone_id, record_id)
.name(&request.name)
.record_type(convert_record_type(&request.record_type))
.content(&request.content)
.proxied(request.proxied);
if let Some(ttl) = request.ttl {
builder = builder.ttl(ttl);
}
if let Some(priority) = request.priority {
builder = builder.priority(priority);
}
let record = builder.send().await.map_err(convert_error)?;
Ok(convert_dns_record(record, zone_id))
}
async fn delete_record(&self, zone_id: &str, record_id: &str) -> PortResult<()> {
self.client
.dns()
.delete_record(zone_id, record_id)
.await
.map_err(convert_error)?;
Ok(())
}
async fn find_record_by_name(
&self,
zone_id: &str,
name: &str,
) -> PortResult<Option<DnsRecord>> {
let records = self
.client
.dns()
.list_records(zone_id)
.name(name)
.send()
.await
.map_err(convert_error)?;
Ok(records
.into_iter()
.next()
.map(|r| convert_dns_record(r, zone_id)))
}
}