diff --git a/Cargo.toml b/Cargo.toml index f64eb4d..5c9b61b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -addr = "0.15.6" -reqwest = { version = "0.11.24", features = ["json"] } -serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.114" -serde_with = "3.6.1" -thiserror = "1.0.57" +addr = "0.15" +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_with = "3" +thiserror = "2" [dev-dependencies] -dotenvy = "0.15.1" -tokio = { version = "1.17.0", features = ["macros"] } +dotenvy = "0.15.7" +tokio = { version = "1.48.0", features = ["macros"] } +rand = "0.9.2" \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 9987220..bb98b6e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,8 +23,13 @@ impl Client { .build() .unwrap(); + let mut base_url = base_url.to_owned(); + if let Some(stripped) = base_url.strip_suffix("/") { + base_url = stripped.to_owned(); + } + Client { - base_url: base_url.to_string(), + base_url, server_name: server_name.to_string(), http_client, } diff --git a/src/server.rs b/src/server.rs index dde9f36..56a782f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -56,10 +56,9 @@ impl<'a> ServerClient<'a> { .http_client .get(format!("{}/api/v1/servers", self.api_client.base_url)) .send() - .await - .unwrap(); + .await?; if resp.status().is_success() { - Ok(resp.json::>().await.unwrap()) + Ok(resp.json::>().await?) } else { Err(resp.json::().await?)? } @@ -91,10 +90,9 @@ impl<'a> ServerClient<'a> { self.api_client.base_url )) .send() - .await - .unwrap(); + .await?; if resp.status().is_success() { - Ok(resp.json::().await.unwrap()) + Ok(resp.json::().await?) } else { Err(resp.json::().await?)? } diff --git a/src/zones.rs b/src/zones.rs index e91e783..875deef 100644 --- a/src/zones.rs +++ b/src/zones.rs @@ -73,6 +73,36 @@ pub enum ZoneKind { } +/// A CreateZone object represents requesst to create authoritative DNS Zone. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde_with::skip_serializing_none] +pub struct CreateZone { + /// Name of the zone (e.g. “example.com.”) MUST have a trailing dot + pub name: Option, + /// Zone kind, one of “Native”, “Master”, “Slave” + pub kind: Option, + /// List of IP addresses configured as a master for this zone (“Slave” type + /// zones only) + pub masters: Option>, + /// Whether or not the zone uses NSEC3 narrow + pub nsec3narrow: Option, + /// Whether or not the zone is pre-signed + pub presigned: Option, + /// The SOA-EDIT metadata item + pub soa_edit: Option, + /// The SOA-EDIT-API metadata item + pub soa_edit_api: Option, + /// MAY contain a BIND-style zone file when creating a zone + pub zone: Option, + /// MAY be set. Its value is defined by local policy + pub account: Option, + /// MAY be sent in client bodies during creation, and MUST NOT be sent by + /// the server. Simple list of strings of nameserver names, including the + /// trailing dot. Not required for slave zones. + pub nameservers: Option>, +} + + /// PatchZones used to create zones with PATCH method. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct PatchZone { @@ -163,8 +193,7 @@ impl<'a> ZoneClient<'a> { self.api_client.base_url, self.api_client.server_name )) .send() - .await - .unwrap(); + .await?; if resp.status().is_success() { Ok(resp.json::>().await?) @@ -184,8 +213,7 @@ impl<'a> ZoneClient<'a> { self.api_client.base_url, self.api_client.server_name )) .send() - .await - .unwrap(); + .await?; if resp.status().is_success() { Ok(resp.json::().await?) @@ -205,8 +233,7 @@ impl<'a> ZoneClient<'a> { self.api_client.base_url, self.api_client.server_name )) .send() - .await - .unwrap(); + .await?; if resp.status().is_success() { Ok(()) @@ -244,6 +271,37 @@ impl<'a> ZoneClient<'a> { status => Err(Error::UnexpectedStatusCode(status)), } } + + + /// Patches zone, by assigning new rrsets to this zone. + pub async fn create(&self, zone: CreateZone) -> Result { + let response = self + .api_client + .http_client + .post( + format!("{}/api/v1/servers/{}/zones", + self.api_client.base_url, + self.api_client.server_name, + )) + .json(&zone) + .send() + .await?; + + match response.status() { + // 201 Created – A zone Returns: Zone object + // 400 Bad Request – The supplied request was not valid Returns: Error object + // 404 Not Found – Requested item was not found Returns: Error object + // 422 Unprocessable Entity – The input to the operation was not valid Returns: Error object + // 500 Internal Server Error – Internal server error Returns: Error object + + StatusCode::CREATED => Ok(response.json::().await?), + StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND | + StatusCode::UNPROCESSABLE_ENTITY | StatusCode::INTERNAL_SERVER_ERROR => { + Err(Error::PowerDNS(response.json().await?)) + }, + status => Err(Error::UnexpectedStatusCode(status)), + } + } } /// Ensure a domain is canonical and top-level @@ -288,3 +346,51 @@ mod tests { assert_eq!(root, "doc.powerdns.com.") } } + + +#[cfg(test)] +mod itests { + use crate::client::Client; + use dotenvy::dotenv; + use std::env; + use crate::zones::{CreateZone, ZoneKind}; + + fn generate_random_domain() -> String { + use rand::Rng; + + rand::rng() + .sample_iter(&rand::distr::Alphanumeric) + .take(32) + .map(char::from) + .collect::() + .to_lowercase() + } + + #[tokio::test] + async fn create() { + dotenv().ok(); + let client = Client::new( + &env::var("PDNS_HOST").unwrap_or_else(|_| String::from("http://localhost:8081")), + &env::var("PDNS_SERVER").unwrap_or_else(|_| String::from("localhost")), + &env::var("PDNS_API_KEY").unwrap(), + ); + + let name = format!("{}.com.", generate_random_domain()); + + let new_zone = client.zone().create(CreateZone { + name: Some(name.clone()), + kind: Some(ZoneKind::Master), + masters: None, + nsec3narrow: None, + presigned: None, + soa_edit: None, + soa_edit_api: None, + zone: None, + account: None, + nameservers: None, + }).await.unwrap(); + + assert_eq!(new_zone.name, Some(name)); + } +} +