add models and api client
Some checks are pending
Build legacy Nix package on Ubuntu / build (push) Waiting to run
Some checks are pending
Build legacy Nix package on Ubuntu / build (push) Waiting to run
This commit is contained in:
parent
da3a2e1dea
commit
7b7bc15241
5 changed files with 317 additions and 3 deletions
1
.envrc
1
.envrc
|
@ -1 +1,2 @@
|
||||||
use flake
|
use flake
|
||||||
|
export HETZNER_API_KEY=pBCqf2QRkrLgFglTgEH4PJMQvxSuNOiH
|
||||||
|
|
|
@ -8,4 +8,4 @@ chrono = { version = "0.4.40", features = ["serde"] }
|
||||||
reqwest = { version = "0.12.15", features = ["json"] }
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }
|
tokio = { version = "1.44.2", features = ["macros", "time"] }
|
||||||
|
|
224
src/client.rs
Normal file
224
src/client.rs
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
use crate::*;
|
||||||
|
use reqwest::{
|
||||||
|
header::{HeaderMap, HeaderValue},
|
||||||
|
Client, Method, Request, Url,
|
||||||
|
};
|
||||||
|
use serde::{Serializer, de::Visitor};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::{borrow::Borrow, collections::HashMap};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum RecordType {
|
||||||
|
A,
|
||||||
|
AAAA,
|
||||||
|
NS,
|
||||||
|
MX,
|
||||||
|
CNAME,
|
||||||
|
RP,
|
||||||
|
TXT,
|
||||||
|
SOA,
|
||||||
|
HINFO,
|
||||||
|
SRV,
|
||||||
|
DANE,
|
||||||
|
TLSA,
|
||||||
|
DS,
|
||||||
|
CAA,
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
impl Serialize for RecordType {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(match self {
|
||||||
|
RecordType::A => "A",
|
||||||
|
RecordType::AAAA => "AAAA",
|
||||||
|
RecordType::NS => "NS",
|
||||||
|
RecordType::MX => "MX",
|
||||||
|
RecordType::CNAME => "CNAME",
|
||||||
|
RecordType::RP => "RP",
|
||||||
|
RecordType::TXT => "TXT",
|
||||||
|
RecordType::SOA => "SOA",
|
||||||
|
RecordType::HINFO => "HINFO",
|
||||||
|
RecordType::SRV => "SRV",
|
||||||
|
RecordType::DANE => "DANE",
|
||||||
|
RecordType::TLSA => "TLSA",
|
||||||
|
RecordType::DS => "DS",
|
||||||
|
RecordType::CAA => "CAA",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for RecordType {
|
||||||
|
type Value = &'static str;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(formatter, "a dns record string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for RecordType {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de> {
|
||||||
|
deserializer.deserialize_str(RecordType)
|
||||||
|
}
|
||||||
|
fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>, {
|
||||||
|
deserializer.deserialize_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
pub struct HetznerDNSAPIClient {
|
||||||
|
token: String,
|
||||||
|
host: &'static str,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HetznerDNSAPIClient {
|
||||||
|
pub fn new(token: String) -> Self {
|
||||||
|
HetznerDNSAPIClient {
|
||||||
|
token,
|
||||||
|
host: "https://dns.hetzner.com/api/v1",
|
||||||
|
client: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_call<'a, T, U, I, K, V>(
|
||||||
|
&self,
|
||||||
|
url: &'a str,
|
||||||
|
method: Method,
|
||||||
|
query: Option<I>,
|
||||||
|
payload: Option<U>,
|
||||||
|
) -> Result<T, ()>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
U: Serialize,
|
||||||
|
I: IntoIterator,
|
||||||
|
<I as IntoIterator>::Item: Borrow<(K, V)>,
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
let mut req = Request::new(method, Url::parse(&url).map_err(|_| ())?);
|
||||||
|
req.headers_mut().append(
|
||||||
|
"Auth-API-Key",
|
||||||
|
HeaderValue::from_str(self.token.as_str()).unwrap(),
|
||||||
|
);
|
||||||
|
if let Some(payload) = payload {
|
||||||
|
*req.body_mut() = Some(serde_json::to_string(&payload).unwrap().into());
|
||||||
|
}
|
||||||
|
Ok(Client::new()
|
||||||
|
.execute(req)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?
|
||||||
|
.json::<T>()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_zones<'a>(
|
||||||
|
&self,
|
||||||
|
name: Option<&'a str>,
|
||||||
|
page: Option<u32>,
|
||||||
|
per_page: Option<u32>,
|
||||||
|
search_name: Option<&'a str>,
|
||||||
|
) -> Result<ZoneResult, ()> {
|
||||||
|
self.api_call(
|
||||||
|
"/zones",
|
||||||
|
Method::GET,
|
||||||
|
Some(&[
|
||||||
|
("name", name.unwrap_or_default()),
|
||||||
|
("page", page.unwrap_or(1).to_string().as_str()),
|
||||||
|
("per_page", per_page.unwrap_or(100).to_string().as_str()),
|
||||||
|
("search_name", search_name.unwrap_or_default()),
|
||||||
|
]),
|
||||||
|
None::<String>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_zone(&self, name: String, ttl: Option<u64>) -> Result<Zone, ()> {
|
||||||
|
self.api_call(
|
||||||
|
"/zones",
|
||||||
|
Method::POST,
|
||||||
|
None::<&[(&str, &str); 0]>,
|
||||||
|
Some(&[("name", name), ("ttl", ttl.unwrap_or(u64::MAX).to_string())]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_zone(&self, id: String) -> Result<Zone, ()> {
|
||||||
|
self.api_call(
|
||||||
|
format!("/zones/{}", id).as_str(),
|
||||||
|
Method::GET,
|
||||||
|
None::<[(&str, &str); 0]>,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_zone(&self, id: String, name: String, ttl: Option<u64>) -> Result<Zone, ()> {
|
||||||
|
self.api_call(
|
||||||
|
format!("/zones/{}", id).as_str(),
|
||||||
|
Method::PUT,
|
||||||
|
None::<[(&str, &str); 0]>,
|
||||||
|
Some(&[("name", name), ("ttl", ttl.unwrap_or(u64::MAX).to_string())]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_zone(&self, id: String) -> Result<(), ()> {
|
||||||
|
self.api_call(
|
||||||
|
format!("/zones/{}", id).as_str(),
|
||||||
|
Method::DELETE,
|
||||||
|
None::<&[(&str, &str); 0]>,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_zone() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn export_zone() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn validate_zone() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_records(&self, page: Option<u32>, per_page: Option<u32>, zone_id: Option<String>) -> Result<Vec<Record>, ()> {
|
||||||
|
let result: RecordsResult = self.api_call("/records", Method::GET, Some(&[("page", page.unwrap_or(1).to_string()), ("per_page", per_page.unwrap_or(100).to_string()), ("zone_id", zone_id.unwrap_or_default())]), None::<u8>).await.map_err(|_| ())?;
|
||||||
|
Ok(result.records)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_record(&self, payload: RecordPayload) -> Result<Record, ()> {
|
||||||
|
let result: RecordResult = self.api_call("/records", Method::POST, None::<[(&str, &str); 0]>, Some(payload)).await.map_err(|_| ())?;
|
||||||
|
Ok(result.record)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_record(&self, record_id: String) -> Result<Record, ()> {
|
||||||
|
let result: RecordResult = self.api_call(format!("/records/{}", record_id).as_str(), Method::GET, None::<[(&str, &str); 0]>, None::<u8>).await.map_err(|_| ())?;
|
||||||
|
Ok(result.record)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_record(&self, record_id: String, payload: RecordPayload) -> Result<Record, ()> {
|
||||||
|
let result: RecordResult = self.api_call(format!("/records/{}", record_id).as_str(), Method::PUT, None::<[(&str, &str); 0]>, Some(payload)).await.map_err(|_| ())?;
|
||||||
|
Ok(result.record)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_record(&self, record_id: String) -> Result<(), ()> {
|
||||||
|
self.api_call(format!("/records/{}", record_id).as_str(), Method::DELETE, None::<[(&str, &str); 0]>, None::<u8>).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||||
|
let result: RecordsResult = self.api_call("/records/bulk", Method::POST, None::<[(&str, &str); 0]>, Some(payloads)).await.map_err(|_| ())?;
|
||||||
|
Ok(result.records)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||||
|
let result: RecordsResult = self.api_call("/records/bulk", Method::PUT, None::<[(&str, &str); 0]>, Some(payloads)).await.map_err(|_| ())?;
|
||||||
|
Ok(result.records)
|
||||||
|
}
|
||||||
|
}
|
13
src/main.rs
13
src/main.rs
|
@ -1,3 +1,12 @@
|
||||||
fn main() {
|
#![allow(dead_code, unused)]
|
||||||
println!("Hello, world!");
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::models::*;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
mod models;
|
||||||
|
mod client;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> () {
|
||||||
}
|
}
|
||||||
|
|
80
src/models.rs
Normal file
80
src/models.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use crate::client::RecordType;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TxtVerification {
|
||||||
|
pub name: String,
|
||||||
|
pub token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Pagination {
|
||||||
|
pub page: u32,
|
||||||
|
pub per_page: u32,
|
||||||
|
pub last_page: u32,
|
||||||
|
pub total_entries: u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Meta {
|
||||||
|
pub pagination: Pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Zone {
|
||||||
|
pub id: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub modified: DateTime<Utc>,
|
||||||
|
pub legacy_dns_host: String,
|
||||||
|
pub legacy_dns: Vec<String>,
|
||||||
|
pub ns: Vec<String>,
|
||||||
|
pub owner: String,
|
||||||
|
pub paused: bool,
|
||||||
|
pub permission: String,
|
||||||
|
pub project: String,
|
||||||
|
pub registrar: String,
|
||||||
|
pub status: String,
|
||||||
|
pub ttl: u32,
|
||||||
|
pub verified: DateTime<Utc>,
|
||||||
|
pub records_count: u32,
|
||||||
|
pub is_secondary_dns: bool,
|
||||||
|
pub txt_verification: Vec<TxtVerification>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ZoneResult {
|
||||||
|
pub zones: Vec<Zone>,
|
||||||
|
pub meta: Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RecordPayload {
|
||||||
|
zone_id: String,
|
||||||
|
r#type: RecordType,
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
ttl: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Record {
|
||||||
|
id: String,
|
||||||
|
created: DateTime<Utc>,
|
||||||
|
modified: DateTime<Utc>,
|
||||||
|
zone_id: String,
|
||||||
|
r#type: String,
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
ttl: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RecordResult {
|
||||||
|
pub record: Record
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RecordsResult {
|
||||||
|
pub records: Vec<Record>
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue