mod types;
use std::{fmt::{Debug, Display}, sync::Mutex};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tracing::{info, warn};
use crate::{
errors::{Error, Result}, generate_helpers, http::{self, ResponseToOption, WithHeaders}, linode::types::{CreateUpdate, Domain, List, Record}, Config, DnsProvider, RecordType
};
const API_BASE: &str = "https://api.linode.com/v4/domains";
#[derive(Clone, Debug, Deserialize)]
pub struct Auth {
pub key: String,
}
impl Auth {
fn get_header(&self) -> String {
format!("Bearer {}", self.key)
}
}
pub struct Linode {
config: Config,
auth: Auth,
domain_id: Mutex<Option<u64>>,
}
impl Linode {
pub fn new(config: Config, auth: Auth) -> Self {
Self {
config,
auth,
domain_id: Mutex::new(None),
}
}
fn get_domain(&self) -> Result<Domain> {
let list = http::client().get(API_BASE)
.with_auth(self.auth.get_header())
.with_json_headers()
.call()?
.to_option::<List<Domain>>()?
.ok_or(Error::ApiError("No domains returned from upstream".to_string()))?;
let domain = list.data.into_iter()
.filter(|d| d.domain == self.config.domain)
.next()
.ok_or(Error::RecordNotFound(self.config.domain.clone()))?;
Ok(domain)
}
fn get_domain_id(&self) -> Result<u64> {
let mut id_p = self.domain_id.lock()
.map_err(|e| Error::LockingError(e.to_string()))?;
if let Some(id) = *id_p {
return Ok(id);
}
let id = self.get_domain()?.id;
*id_p = Some(id);
Ok(id)
}
fn get_upstream_record<T>(&self, rtype: &RecordType, host: &str) -> Result<Option<Record<T>>>
where T: DeserializeOwned
{
let did = self.get_domain_id()?;
let url = format!("{API_BASE}/{did}/records");
let mut response = http::client().get(url)
.with_auth(self.auth.get_header())
.with_json_headers()
.call()?;
let body = response.body_mut().read_to_string()?;
let srtype = rtype.to_string();
let values: serde_json::Value = serde_json::from_str(&body)?;
let data = values["data"].as_array()
.ok_or(Error::ApiError("Data field not found".to_string()))?;
let record = data.into_iter()
.filter_map(|obj| match &obj["type"] {
serde_json::Value::String(t)
if t == &srtype && obj["name"] == host
=> Some(serde_json::from_value(obj.clone())),
_ => None,
})
.next()
.transpose()?;
Ok(record)
}
fn get_record_id(&self, rtype: &RecordType, host: &str) -> Result<Option<u64>>
{
Ok(self.get_upstream_record::<String>(rtype, host)?
.map(|r| r.id))
}
}
impl DnsProvider for Linode {
fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
where
T: DeserializeOwned
{
let rec = match self.get_upstream_record(&rtype, host)? {
Some(rec) => rec,
None => return Ok(None)
};
Ok(Some(rec.target))
}
fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
where
T: Serialize + DeserializeOwned + Display + Clone
{
let did = self.get_domain_id()?;
let url = format!("{API_BASE}/{did}/records");
let create = CreateUpdate {
name: host.to_string(),
rtype,
target: record.to_string(),
ttl_sec: 300,
};
if self.config.dry_run {
info!("DRY-RUN: Would have sent {create:?} to {url}");
return Ok(())
}
let body = serde_json::to_string(&create)?;
let _response = http::client().post(url)
.with_auth(self.auth.get_header())
.with_json_headers()
.send(body)?
.check_error()?;
Ok(())
}
fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
where
T: Serialize + DeserializeOwned + Display + Clone
{
let did = self.get_domain_id()?;
let id = self.get_record_id(&rtype, host)?
.ok_or(Error::RecordNotFound(host.to_string()))?;
let url = format!("{API_BASE}/{did}/records/{id}");
let update = CreateUpdate {
name: host.to_string(),
rtype,
target: urec.to_string(),
ttl_sec: 300,
};
if self.config.dry_run {
info!("DRY-RUN: Would have sent {update:?} to {url}");
return Ok(())
}
let body = serde_json::to_string(&update)?;
let _response = http::client().put(url)
.with_auth(self.auth.get_header())
.with_json_headers()
.send(body)?
.check_error()?;
Ok(())
}
fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
{
let id = match self.get_record_id(&rtype, host)? {
Some(id) => id,
None => {
warn!("No {rtype} record to delete for {host}");
return Ok(());
}
};
let did = self.get_domain_id()?;
let url = format!("{API_BASE}/{did}/records/{id}");
if self.config.dry_run {
info!("DRY-RUN: Would have sent DELETE to {url}");
return Ok(())
}
http::client().delete(url)
.with_auth(self.auth.get_header())
.with_json_headers()
.call()?;
Ok(())
}
generate_helpers!();
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{generate_tests, tests::*};
use std::env;
fn get_client() -> Linode {
let auth = Auth {
key: env::var("LINODE_API_KEY").unwrap(),
};
let config = Config {
domain: env::var("LINODE_TEST_DOMAIN").unwrap(),
dry_run: false,
};
Linode::new(config, auth)
}
generate_tests!("test_linode");
}