add models and api client
Some checks are pending
Build legacy Nix package on Ubuntu / build (push) Waiting to run

This commit is contained in:
bread 2025-04-19 18:50:19 +02:00
parent da3a2e1dea
commit 7b7bc15241
5 changed files with 317 additions and 3 deletions

1
.envrc
View file

@ -1 +1,2 @@
use flake
export HETZNER_API_KEY=pBCqf2QRkrLgFglTgEH4PJMQvxSuNOiH

View file

@ -8,4 +8,4 @@ chrono = { version = "0.4.40", features = ["serde"] }
reqwest = { version = "0.12.15", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
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
View 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)
}
}

View file

@ -1,3 +1,12 @@
fn main() {
println!("Hello, world!");
#![allow(dead_code, unused)]
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
View 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>
}