fix multi name support, create if not existing, update if existing, more type fixing
All checks were successful
Build legacy Nix package on Ubuntu / build (push) Successful in 13m22s
All checks were successful
Build legacy Nix package on Ubuntu / build (push) Successful in 13m22s
This commit is contained in:
parent
948da79c12
commit
e9c05d8c7c
3 changed files with 314 additions and 163 deletions
225
src/client.rs
225
src/client.rs
|
@ -1,75 +1,13 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use reqwest::{
|
use reqwest::{Client, Method, Request, StatusCode, Url, header::HeaderValue};
|
||||||
Client, Method, Request, Url,
|
use std::{borrow::Borrow, fmt};
|
||||||
header::HeaderValue,
|
|
||||||
};
|
|
||||||
use std::borrow::Borrow;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum RecordType {
|
|
||||||
A,
|
|
||||||
AAAA,
|
|
||||||
NS,
|
|
||||||
MX,
|
|
||||||
CNAME,
|
|
||||||
RP,
|
|
||||||
TXT,
|
|
||||||
SOA,
|
|
||||||
HINFO,
|
|
||||||
SRV,
|
|
||||||
DANE,
|
|
||||||
TLSA,
|
|
||||||
DS,
|
|
||||||
CAA,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for RecordType {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
RecordType::A => "A".to_string(),
|
|
||||||
RecordType::AAAA => "AAAA".to_string(),
|
|
||||||
RecordType::NS => "NS".to_string(),
|
|
||||||
RecordType::MX => "MX".to_string(),
|
|
||||||
RecordType::CNAME => "CNAME".to_string(),
|
|
||||||
RecordType::RP => "RP".to_string(),
|
|
||||||
RecordType::TXT => "TXT".to_string(),
|
|
||||||
RecordType::SOA => "SOA".to_string(),
|
|
||||||
RecordType::HINFO => "HINFO".to_string(),
|
|
||||||
RecordType::SRV => "SRV".to_string(),
|
|
||||||
RecordType::DANE => "DANE".to_string(),
|
|
||||||
RecordType::TLSA => "TLSA".to_string(),
|
|
||||||
RecordType::DS => "DS".to_string(),
|
|
||||||
RecordType::CAA => "CAA".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for RecordType {
|
|
||||||
type Error = &'static str;
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
Ok(match value {
|
|
||||||
"A" => RecordType::A,
|
|
||||||
"AAAA" => RecordType::AAAA,
|
|
||||||
"NS" => RecordType::NS,
|
|
||||||
"MX" => RecordType::MX,
|
|
||||||
"CNAME" => RecordType::CNAME,
|
|
||||||
"RP" => RecordType::RP,
|
|
||||||
"TXT" => RecordType::TXT,
|
|
||||||
"SOA" => RecordType::SOA,
|
|
||||||
"HINFO" => RecordType::HINFO,
|
|
||||||
"SRV" => RecordType::SRV,
|
|
||||||
"DANE" => RecordType::DANE,
|
|
||||||
"TLSA" => RecordType::TLSA,
|
|
||||||
"DS" => RecordType::DS,
|
|
||||||
"CAA" => RecordType::CAA,
|
|
||||||
_ => return Err(""),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct _RecordQuery {
|
struct _RecordQuery<T>
|
||||||
records: Vec<RecordPayload>,
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
records: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HetznerDNSAPIClient {
|
pub struct HetznerDNSAPIClient {
|
||||||
|
@ -78,6 +16,48 @@ pub struct HetznerDNSAPIClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HetznerDNSAPIError {
|
||||||
|
Unauthorized,
|
||||||
|
Forbidden,
|
||||||
|
NotFound,
|
||||||
|
NotAcceptable,
|
||||||
|
Conflict,
|
||||||
|
UnprocessableEntity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for HetznerDNSAPIError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Unauthorized => "Missing authorization, did you set a token?",
|
||||||
|
Self::Forbidden => "Forbidden action!",
|
||||||
|
Self::NotFound => "Entity not found!",
|
||||||
|
Self::NotAcceptable => "Request failed, possibly bad input!",
|
||||||
|
Self::Conflict => "Conflict encountered",
|
||||||
|
Self::UnprocessableEntity =>
|
||||||
|
"Request not procesasble, input is invalid or might already exist!",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<StatusCode> for HetznerDNSAPIError {
|
||||||
|
type Error = String;
|
||||||
|
fn try_from(value: StatusCode) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
StatusCode::UNAUTHORIZED => Self::Unauthorized,
|
||||||
|
StatusCode::FORBIDDEN => Self::Forbidden,
|
||||||
|
StatusCode::NOT_FOUND => Self::NotFound,
|
||||||
|
StatusCode::NOT_ACCEPTABLE => Self::NotAcceptable,
|
||||||
|
StatusCode::CONFLICT => Self::Conflict,
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY => Self::UnprocessableEntity,
|
||||||
|
_ => return Err(format!("Invalid error code: {}", value)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl HetznerDNSAPIClient {
|
impl HetznerDNSAPIClient {
|
||||||
pub fn new(token: String) -> Self {
|
pub fn new(token: String) -> Self {
|
||||||
HetznerDNSAPIClient {
|
HetznerDNSAPIClient {
|
||||||
|
@ -87,13 +67,13 @@ impl HetznerDNSAPIClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn api_call<'a, T, U, I, K, V>(
|
async fn api_call<'a, T, U, I, K, V>(
|
||||||
&self,
|
&self,
|
||||||
url: &'a str,
|
url: &'a str,
|
||||||
method: Method,
|
method: Method,
|
||||||
query: Option<I>,
|
query: Option<I>,
|
||||||
payload: Option<U>,
|
payload: Option<U>,
|
||||||
) -> Result<T, ()>
|
) -> Result<T, HetznerDNSAPIError>
|
||||||
where
|
where
|
||||||
T: for<'de> Deserialize<'de>,
|
T: for<'de> Deserialize<'de>,
|
||||||
U: Serialize,
|
U: Serialize,
|
||||||
|
@ -106,6 +86,7 @@ impl HetznerDNSAPIClient {
|
||||||
method,
|
method,
|
||||||
self.host.join(url).map_err(|e| {
|
self.host.join(url).map_err(|e| {
|
||||||
println!("url formatting error: {}", e);
|
println!("url formatting error: {}", e);
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
req.headers_mut().append(
|
req.headers_mut().append(
|
||||||
|
@ -117,9 +98,16 @@ impl HetznerDNSAPIClient {
|
||||||
serde_json::to_string(&payload)
|
serde_json::to_string(&payload)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
println!("body encoding error: {}", e);
|
println!("body encoding error: {}", e);
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity
|
||||||
})?
|
})?
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
//println!("{}", serde_json::to_string_pretty(&payload).unwrap());
|
||||||
|
}
|
||||||
|
if let Some(query) = query {
|
||||||
|
req.url_mut()
|
||||||
|
.query_pairs_mut()
|
||||||
|
.extend_pairs(query.into_iter());
|
||||||
}
|
}
|
||||||
let t = self
|
let t = self
|
||||||
.client
|
.client
|
||||||
|
@ -127,18 +115,19 @@ impl HetznerDNSAPIClient {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
println!("request execution error: {}", e);
|
println!("request execution error: {}", e);
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity
|
||||||
})?
|
})?
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
.map_err(|e| {
|
.map_err(|e| HetznerDNSAPIError::try_from(e.status().unwrap()).unwrap())?
|
||||||
println!("request error: {}", e);
|
|
||||||
})?
|
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
println!("request decoding error: {}", e);
|
println!("request decoding error: {}", e);
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity
|
||||||
})?;
|
})?;
|
||||||
serde_json::from_str::<T>(&t).map_err(|e| {
|
serde_json::from_str::<T>(&t).map_err(|e| {
|
||||||
println!("json response parsing error: {}", e);
|
println!("json response parsing error: {}", e);
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +137,7 @@ impl HetznerDNSAPIClient {
|
||||||
page: Option<u32>,
|
page: Option<u32>,
|
||||||
per_page: Option<u32>,
|
per_page: Option<u32>,
|
||||||
search_name: Option<&'a str>,
|
search_name: Option<&'a str>,
|
||||||
) -> Result<Vec<Zone>, ()> {
|
) -> Result<Vec<Zone>, HetznerDNSAPIError> {
|
||||||
let result: ZoneResult = self
|
let result: ZoneResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
"zones",
|
"zones",
|
||||||
|
@ -165,7 +154,11 @@ impl HetznerDNSAPIClient {
|
||||||
Ok(result.zones)
|
Ok(result.zones)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_zone(&self, name: String, ttl: Option<u64>) -> Result<Zone, ()> {
|
pub async fn create_zone(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
ttl: Option<u64>,
|
||||||
|
) -> Result<Zone, HetznerDNSAPIError> {
|
||||||
self.api_call(
|
self.api_call(
|
||||||
"zones",
|
"zones",
|
||||||
Method::POST,
|
Method::POST,
|
||||||
|
@ -175,7 +168,7 @@ impl HetznerDNSAPIClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_zone(&self, id: String) -> Result<Zone, ()> {
|
pub async fn get_zone(&self, id: String) -> Result<Zone, HetznerDNSAPIError> {
|
||||||
self.api_call(
|
self.api_call(
|
||||||
format!("zones/{}", id).as_str(),
|
format!("zones/{}", id).as_str(),
|
||||||
Method::GET,
|
Method::GET,
|
||||||
|
@ -190,7 +183,7 @@ impl HetznerDNSAPIClient {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
ttl: Option<u64>,
|
ttl: Option<u64>,
|
||||||
) -> Result<Zone, ()> {
|
) -> Result<Zone, HetznerDNSAPIError> {
|
||||||
self.api_call(
|
self.api_call(
|
||||||
format!("zones/{}", id).as_str(),
|
format!("zones/{}", id).as_str(),
|
||||||
Method::PUT,
|
Method::PUT,
|
||||||
|
@ -200,14 +193,16 @@ impl HetznerDNSAPIClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_zone(&self, id: String) -> Result<(), ()> {
|
pub async fn delete_zone(&self, id: String) -> Result<(), HetznerDNSAPIError> {
|
||||||
self.api_call(
|
let result: () = self
|
||||||
format!("zones/{}", id).as_str(),
|
.api_call(
|
||||||
Method::DELETE,
|
format!("zones/{}", id).as_str(),
|
||||||
None::<&[(&str, &str); 0]>,
|
Method::DELETE,
|
||||||
None::<&str>,
|
None::<&[(&str, &str); 0]>,
|
||||||
)
|
None::<&str>,
|
||||||
.await?
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn import_zone() {
|
pub async fn import_zone() {
|
||||||
|
@ -225,7 +220,7 @@ impl HetznerDNSAPIClient {
|
||||||
page: Option<u32>,
|
page: Option<u32>,
|
||||||
per_page: Option<u32>,
|
per_page: Option<u32>,
|
||||||
zone_id: Option<String>,
|
zone_id: Option<String>,
|
||||||
) -> Result<Vec<Record>, ()> {
|
) -> Result<Vec<Record>, HetznerDNSAPIError> {
|
||||||
let result: RecordsResult = self
|
let result: RecordsResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
"records",
|
"records",
|
||||||
|
@ -237,12 +232,14 @@ impl HetznerDNSAPIClient {
|
||||||
]),
|
]),
|
||||||
None::<u8>,
|
None::<u8>,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.records)
|
Ok(result.records)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_record(&self, payload: RecordPayload) -> Result<Record, ()> {
|
pub async fn create_record(
|
||||||
|
&self,
|
||||||
|
payload: RecordPayload,
|
||||||
|
) -> Result<Record, HetznerDNSAPIError> {
|
||||||
let result: RecordResult = self
|
let result: RecordResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
"records",
|
"records",
|
||||||
|
@ -250,12 +247,11 @@ impl HetznerDNSAPIClient {
|
||||||
None::<[(&str, &str); 0]>,
|
None::<[(&str, &str); 0]>,
|
||||||
Some(payload),
|
Some(payload),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.record)
|
Ok(result.record)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_record(&self, record_id: String) -> Result<Record, ()> {
|
pub async fn get_record(&self, record_id: String) -> Result<Record, HetznerDNSAPIError> {
|
||||||
let result: RecordResult = self
|
let result: RecordResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
format!("records/{}", record_id).as_str(),
|
format!("records/{}", record_id).as_str(),
|
||||||
|
@ -263,8 +259,7 @@ impl HetznerDNSAPIClient {
|
||||||
None::<[(&str, &str); 0]>,
|
None::<[(&str, &str); 0]>,
|
||||||
None::<u8>,
|
None::<u8>,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.record)
|
Ok(result.record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,7 +267,7 @@ impl HetznerDNSAPIClient {
|
||||||
&self,
|
&self,
|
||||||
record_id: String,
|
record_id: String,
|
||||||
payload: RecordPayload,
|
payload: RecordPayload,
|
||||||
) -> Result<Record, ()> {
|
) -> Result<Record, HetznerDNSAPIError> {
|
||||||
let result: RecordResult = self
|
let result: RecordResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
format!("records/{}", record_id).as_str(),
|
format!("records/{}", record_id).as_str(),
|
||||||
|
@ -280,22 +275,26 @@ impl HetznerDNSAPIClient {
|
||||||
None::<[(&str, &str); 0]>,
|
None::<[(&str, &str); 0]>,
|
||||||
Some(payload),
|
Some(payload),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.record)
|
Ok(result.record)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_record(&self, record_id: String) -> Result<(), ()> {
|
pub async fn delete_record(&self, record_id: String) -> Result<(), HetznerDNSAPIError> {
|
||||||
self.api_call(
|
let result: () = self
|
||||||
format!("records/{}", record_id).as_str(),
|
.api_call(
|
||||||
Method::DELETE,
|
format!("records/{}", record_id).as_str(),
|
||||||
None::<[(&str, &str); 0]>,
|
Method::DELETE,
|
||||||
None::<u8>,
|
None::<[(&str, &str); 0]>,
|
||||||
)
|
None::<u8>,
|
||||||
.await?
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
pub async fn create_records(
|
||||||
|
&self,
|
||||||
|
payloads: Vec<RecordPayload>,
|
||||||
|
) -> Result<Vec<Record>, HetznerDNSAPIError> {
|
||||||
let result: RecordsResult = self
|
let result: RecordsResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
"records/bulk",
|
"records/bulk",
|
||||||
|
@ -303,24 +302,22 @@ impl HetznerDNSAPIClient {
|
||||||
None::<[(&str, &str); 0]>,
|
None::<[(&str, &str); 0]>,
|
||||||
Some(_RecordQuery { records: payloads }),
|
Some(_RecordQuery { records: payloads }),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.records)
|
Ok(result.records)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_records(
|
pub async fn update_records(
|
||||||
&self,
|
&self,
|
||||||
payloads: Vec<(String, RecordPayload)>,
|
payloads: Vec<RecordPayload>,
|
||||||
) -> Result<Vec<Record>, ()> {
|
) -> Result<Vec<Record>, HetznerDNSAPIError> {
|
||||||
let result: RecordsResult = self
|
let result: RecordsResult = self
|
||||||
.api_call(
|
.api_call(
|
||||||
"records/bulk",
|
"records/bulk",
|
||||||
Method::PUT,
|
Method::PUT,
|
||||||
None::<[(&str, &str); 0]>,
|
None::<[(&str, &str); 0]>,
|
||||||
Some(payloads),
|
Some(_RecordQuery { records: payloads }),
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
Ok(result.records)
|
Ok(result.records)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
154
src/main.rs
154
src/main.rs
|
@ -24,21 +24,18 @@ enum SubMode {
|
||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ZoneContext {
|
struct ZoneContext {
|
||||||
all: bool,
|
all: bool,
|
||||||
zone: String,
|
zone: String,
|
||||||
name: String,
|
name: String,
|
||||||
ttl: u64,
|
ttl: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct RecordContext {
|
struct RecordContext {
|
||||||
all: bool,
|
all: bool,
|
||||||
records: Vec<RecordPayload>,
|
records: Vec<RecordPayload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Context {
|
struct Context {
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
submode: SubMode,
|
submode: SubMode,
|
||||||
|
@ -57,16 +54,17 @@ async fn main() {
|
||||||
all: false,
|
all: false,
|
||||||
zone: String::new(),
|
zone: String::new(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
ttl: 86400,
|
ttl: None,
|
||||||
},
|
},
|
||||||
record_context: RecordContext {
|
record_context: RecordContext {
|
||||||
all: false,
|
all: false,
|
||||||
records: vec![RecordPayload {
|
records: vec![RecordPayload {
|
||||||
|
id: None,
|
||||||
zone_id: String::new(),
|
zone_id: String::new(),
|
||||||
r#type: RecordType::A,
|
r#type: RecordType::A,
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
value: String::new(),
|
value: String::new(),
|
||||||
ttl: 0,
|
ttl: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -110,11 +108,8 @@ async fn main() {
|
||||||
ctx.zone_context.name = std::env::args().nth(idx + 1).unwrap()
|
ctx.zone_context.name = std::env::args().nth(idx + 1).unwrap()
|
||||||
}
|
}
|
||||||
"--ttl" => {
|
"--ttl" => {
|
||||||
ctx.zone_context.ttl = std::env::args()
|
ctx.zone_context.ttl =
|
||||||
.nth(idx + 1)
|
std::env::args().nth(idx + 1).unwrap().parse().ok()
|
||||||
.unwrap()
|
|
||||||
.parse()
|
|
||||||
.unwrap_or(86400)
|
|
||||||
}
|
}
|
||||||
"--zone" => {
|
"--zone" => {
|
||||||
ctx.zone_context.zone = std::env::args().nth(idx + 1).unwrap()
|
ctx.zone_context.zone = std::env::args().nth(idx + 1).unwrap()
|
||||||
|
@ -148,11 +143,8 @@ async fn main() {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
"--ttl" => {
|
"--ttl" => {
|
||||||
ctx.record_context.records[0].ttl = std::env::args()
|
ctx.record_context.records[0].ttl =
|
||||||
.nth(idx + 1)
|
std::env::args().nth(idx + 1).unwrap().parse().ok()
|
||||||
.unwrap()
|
|
||||||
.parse()
|
|
||||||
.unwrap_or(86400)
|
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
@ -192,7 +184,7 @@ async fn main() {
|
||||||
SubMode::Create => {
|
SubMode::Create => {
|
||||||
if !ctx.zone_context.name.is_empty() {
|
if !ctx.zone_context.name.is_empty() {
|
||||||
if let Ok(zone) = client
|
if let Ok(zone) = client
|
||||||
.create_zone(ctx.zone_context.name, Some(ctx.zone_context.ttl))
|
.create_zone(ctx.zone_context.name, ctx.zone_context.ttl)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("{:#?}", zone);
|
println!("{:#?}", zone);
|
||||||
|
@ -214,7 +206,7 @@ async fn main() {
|
||||||
.update_zone(
|
.update_zone(
|
||||||
zones.into_iter().next().unwrap().id,
|
zones.into_iter().next().unwrap().id,
|
||||||
ctx.zone_context.name,
|
ctx.zone_context.name,
|
||||||
Some(ctx.zone_context.ttl),
|
ctx.zone_context.ttl,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -285,15 +277,17 @@ async fn main() {
|
||||||
.unwrap()[0]
|
.unwrap()[0]
|
||||||
.id;
|
.id;
|
||||||
ctx.record_context.records[0].zone_id = zone.to_string();
|
ctx.record_context.records[0].zone_id = zone.to_string();
|
||||||
if let Ok(record) = client
|
match client
|
||||||
.create_records(
|
.create_records(
|
||||||
ctx.record_context
|
ctx.record_context
|
||||||
.records
|
.records
|
||||||
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|r| {
|
.flat_map(|r| {
|
||||||
r.name
|
r.name
|
||||||
.split(",")
|
.split(",")
|
||||||
.map(|s| RecordPayload {
|
.map(|s| RecordPayload {
|
||||||
|
id: None,
|
||||||
zone_id: r.zone_id.clone(),
|
zone_id: r.zone_id.clone(),
|
||||||
r#type: r.r#type.clone(),
|
r#type: r.r#type.clone(),
|
||||||
name: String::from(s),
|
name: String::from(s),
|
||||||
|
@ -306,7 +300,61 @@ async fn main() {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("{:#?}", record);
|
Ok(records) => records.into_iter().for_each(|r| println!("{}", r)),
|
||||||
|
Err(e) => match e {
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity => {
|
||||||
|
eprintln!("Records already exist, updating instead...");
|
||||||
|
if let Ok(existing) = client
|
||||||
|
.get_records(None, None, Some(zone.to_string()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
existing
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|r| println!("{}", r));
|
||||||
|
let records = client
|
||||||
|
.update_records(
|
||||||
|
existing
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| {
|
||||||
|
ctx.record_context
|
||||||
|
.records
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.any(|o| {
|
||||||
|
o.name
|
||||||
|
.split(",")
|
||||||
|
.any(|s| s == r.name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|r| RecordPayload {
|
||||||
|
id: Some(r.id.clone()),
|
||||||
|
zone_id: r.zone_id,
|
||||||
|
r#type: r.r#type,
|
||||||
|
name: r.name.clone(),
|
||||||
|
value: ctx
|
||||||
|
.record_context
|
||||||
|
.records
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.find(|o| {
|
||||||
|
o.name
|
||||||
|
.split(",")
|
||||||
|
.any(|s| s == r.name)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.value,
|
||||||
|
ttl: r.ttl,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
records.into_iter().for_each(|r| println!("{}", r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => eprintln!("{}", e),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -314,7 +362,7 @@ async fn main() {
|
||||||
if !ctx.record_context.records.is_empty() {
|
if !ctx.record_context.records.is_empty() {
|
||||||
let mut records = vec![];
|
let mut records = vec![];
|
||||||
let mut updated_records = vec![];
|
let mut updated_records = vec![];
|
||||||
let mut records_iter = ctx.record_context.records.into_iter();
|
let records_iter = ctx.record_context.records.into_iter();
|
||||||
for zone in records_iter
|
for zone in records_iter
|
||||||
.clone()
|
.clone()
|
||||||
.map(|r| r.zone_id)
|
.map(|r| r.zone_id)
|
||||||
|
@ -331,29 +379,55 @@ async fn main() {
|
||||||
client.get_records(None, None, Some(zone.id)).await.unwrap(),
|
client.get_records(None, None, Some(zone.id)).await.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for old_record in records {
|
for old_record in &records {
|
||||||
if let Some(new_record) = records_iter.find(|r| {
|
if let Some(new_record) =
|
||||||
r.name == old_record.name
|
records_iter.clone().find(|r| r.name == old_record.name)
|
||||||
|| r.name
|
{
|
||||||
.split(",")
|
updated_records.push(RecordPayload {
|
||||||
.find(|s| *s == old_record.name)
|
id: Some(old_record.id.clone()),
|
||||||
.is_some()
|
zone_id: old_record.zone_id.clone(),
|
||||||
}) {
|
r#type: old_record.r#type.clone(),
|
||||||
updated_records.push((
|
name: old_record.name.clone(),
|
||||||
old_record.id,
|
value: new_record.value,
|
||||||
RecordPayload {
|
ttl: new_record.ttl,
|
||||||
zone_id: old_record.zone_id,
|
});
|
||||||
r#type: old_record.r#type,
|
} else if let Some(new_record) = records_iter
|
||||||
name: old_record.name,
|
.clone()
|
||||||
value: new_record.value,
|
.find(|r| r.name.split(",").any(|s| s == old_record.name))
|
||||||
|
{
|
||||||
|
for name in new_record.name.split(",") {
|
||||||
|
updated_records.push(RecordPayload {
|
||||||
|
id: Some(old_record.id.clone()),
|
||||||
|
zone_id: old_record.zone_id.clone(),
|
||||||
|
r#type: old_record.r#type.clone(),
|
||||||
|
name: name.to_string(),
|
||||||
|
value: new_record.value.clone(),
|
||||||
ttl: new_record.ttl,
|
ttl: new_record.ttl,
|
||||||
},
|
});
|
||||||
));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !updated_records.is_empty() {
|
if !updated_records.is_empty() {
|
||||||
let records = client.update_records(updated_records).await.unwrap();
|
let new_records =
|
||||||
eprintln!("Updated {} records", records.len());
|
client.update_records(updated_records.clone()).await;
|
||||||
|
match new_records {
|
||||||
|
Ok(records) => {
|
||||||
|
records.into_iter().for_each(|r| println!("{}", r))
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
HetznerDNSAPIError::UnprocessableEntity => {
|
||||||
|
eprintln!(
|
||||||
|
"Updating failed, trying to create them first."
|
||||||
|
);
|
||||||
|
let records = client
|
||||||
|
.create_records(updated_records)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
records.into_iter().for_each(|r| println!("{}", r));
|
||||||
|
}
|
||||||
|
_ => eprintln!("{}", e),
|
||||||
|
},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"No records found that require updating. Did you mean to create/c the records instead?"
|
"No records found that require updating. Did you mean to create/c the records instead?"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::client::RecordType;
|
#[warn(unused)]
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::borrow::Borrow;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct TxtVerification {
|
pub struct TxtVerification {
|
||||||
|
@ -9,7 +9,7 @@ pub struct TxtVerification {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Pagination {
|
pub struct Pagination {
|
||||||
pub page: u32,
|
pub page: u32,
|
||||||
pub per_page: u32,
|
pub per_page: u32,
|
||||||
|
@ -17,7 +17,7 @@ pub struct Pagination {
|
||||||
pub total_entries: u32,
|
pub total_entries: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Meta {
|
pub struct Meta {
|
||||||
pub pagination: Pagination,
|
pub pagination: Pagination,
|
||||||
}
|
}
|
||||||
|
@ -46,22 +46,89 @@ pub struct Zone {
|
||||||
pub txt_verification: TxtVerification,
|
pub txt_verification: TxtVerification,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ZoneResult {
|
pub struct ZoneResult {
|
||||||
pub zones: Vec<Zone>,
|
pub zones: Vec<Zone>,
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum RecordType {
|
||||||
|
A,
|
||||||
|
Aaaa,
|
||||||
|
Ns,
|
||||||
|
Mx,
|
||||||
|
Cname,
|
||||||
|
Rp,
|
||||||
|
Txt,
|
||||||
|
Soa,
|
||||||
|
Hinfo,
|
||||||
|
Srv,
|
||||||
|
Dane,
|
||||||
|
Tlsa,
|
||||||
|
Ds,
|
||||||
|
Caa,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RecordType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::A => "A",
|
||||||
|
Self::Aaaa => "AAAA",
|
||||||
|
Self::Ns => "NS",
|
||||||
|
Self::Mx => "MX",
|
||||||
|
Self::Cname => "CNAME",
|
||||||
|
Self::Rp => "RP",
|
||||||
|
Self::Txt => "TXT",
|
||||||
|
Self::Soa => "SOA",
|
||||||
|
Self::Hinfo => "HINFO",
|
||||||
|
Self::Srv => "SRV",
|
||||||
|
Self::Dane => "DANE",
|
||||||
|
Self::Tlsa => "TLSA",
|
||||||
|
Self::Ds => "DS",
|
||||||
|
Self::Caa => "CAA",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for RecordType {
|
||||||
|
type Error = &'static str;
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
Ok(match value {
|
||||||
|
"A" => Self::A,
|
||||||
|
"AAAA" => Self::Aaaa,
|
||||||
|
"NS" => Self::Ns,
|
||||||
|
"MX" => Self::Mx,
|
||||||
|
"CNAME" => Self::Cname,
|
||||||
|
"RP" => Self::Rp,
|
||||||
|
"TXT" => Self::Txt,
|
||||||
|
"SOA" => Self::Soa,
|
||||||
|
"HINFO" => Self::Hinfo,
|
||||||
|
"SRV" => Self::Srv,
|
||||||
|
"DANE" => Self::Dane,
|
||||||
|
"TLSA" => Self::Tlsa,
|
||||||
|
"DS" => Self::Ds,
|
||||||
|
"CAA" => Self::Caa,
|
||||||
|
_ => return Err(""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct RecordPayload {
|
pub struct RecordPayload {
|
||||||
|
pub id: Option<String>,
|
||||||
pub zone_id: String,
|
pub zone_id: String,
|
||||||
pub r#type: RecordType,
|
pub r#type: RecordType,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub ttl: u64,
|
pub ttl: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Record {
|
pub struct Record {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[serde(with = "hetzner_date")]
|
#[serde(with = "hetzner_date")]
|
||||||
|
@ -75,12 +142,25 @@ pub struct Record {
|
||||||
pub ttl: Option<u64>,
|
pub ttl: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
impl fmt::Display for Record {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} | {} | {} | {}",
|
||||||
|
self.name,
|
||||||
|
self.r#type,
|
||||||
|
self.value,
|
||||||
|
self.ttl.unwrap_or_default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct RecordResult {
|
pub struct RecordResult {
|
||||||
pub record: Record,
|
pub record: Record,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct RecordsResult {
|
pub struct RecordsResult {
|
||||||
pub records: Vec<Record>,
|
pub records: Vec<Record>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue