Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 6 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
10 changes: 4 additions & 6 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<Server>>().await.unwrap())
Ok(resp.json::<Vec<Server>>().await?)
} else {
Err(resp.json::<PowerDNSResponseError>().await?)?
}
Expand Down Expand Up @@ -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::<Server>().await.unwrap())
Ok(resp.json::<Server>().await?)
} else {
Err(resp.json::<PowerDNSResponseError>().await?)?
}
Expand Down
118 changes: 112 additions & 6 deletions src/zones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Zone kind, one of “Native”, “Master”, “Slave”
pub kind: Option<ZoneKind>,
/// List of IP addresses configured as a master for this zone (“Slave” type
/// zones only)
pub masters: Option<Vec<String>>,
/// Whether or not the zone uses NSEC3 narrow
pub nsec3narrow: Option<bool>,
/// Whether or not the zone is pre-signed
pub presigned: Option<bool>,
/// The SOA-EDIT metadata item
pub soa_edit: Option<String>,
/// The SOA-EDIT-API metadata item
pub soa_edit_api: Option<String>,
/// MAY contain a BIND-style zone file when creating a zone
pub zone: Option<String>,
/// MAY be set. Its value is defined by local policy
pub account: Option<String>,
/// 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<Vec<String>>,
}


/// PatchZones used to create zones with PATCH method.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PatchZone {
Expand Down Expand Up @@ -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::<Vec<Zone>>().await?)
Expand All @@ -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::<Zone>().await?)
Expand All @@ -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(())
Expand Down Expand Up @@ -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<Zone, Error> {
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::<Zone>().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
Expand Down Expand Up @@ -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::<String>()
.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));
}
}