add command and various bug fixes (tests needed aaaa)
Some checks failed
Build legacy Nix package on Ubuntu / build (push) Failing after 1m18s
Some checks failed
Build legacy Nix package on Ubuntu / build (push) Failing after 1m18s
This commit is contained in:
parent
e1835d0f2c
commit
9e42f2e53d
6 changed files with 297 additions and 52 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hetzner_ddns"
|
||||
name = "hetzner_dns"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "hetzner_ddns"
|
||||
name = "hetzner_dns"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
|
|
100
src/client.rs
100
src/client.rs
|
@ -72,7 +72,7 @@ impl<'de> Deserialize<'de> for RecordType {
|
|||
*/
|
||||
pub struct HetznerDNSAPIClient {
|
||||
token: String,
|
||||
host: &'static str,
|
||||
host: Url,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
|
@ -80,12 +80,12 @@ impl HetznerDNSAPIClient {
|
|||
pub fn new(token: String) -> Self {
|
||||
HetznerDNSAPIClient {
|
||||
token,
|
||||
host: "https://dns.hetzner.com/api/v1",
|
||||
host: Url::parse("https://dns.hetzner.com/api/v1/").unwrap(),
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_call<'a, T, U, I, K, V>(
|
||||
pub async fn api_call<'a, T, U, I, K, V>(
|
||||
&self,
|
||||
url: &'a str,
|
||||
method: Method,
|
||||
|
@ -100,24 +100,35 @@ impl HetznerDNSAPIClient {
|
|||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let mut req = Request::new(method, Url::parse(&url).map_err(|_| ())?);
|
||||
let mut req = Request::new(
|
||||
method,
|
||||
self.host.join(url).map_err(|e| {
|
||||
println!("url formatting error: {}", e);
|
||||
()
|
||||
})?,
|
||||
);
|
||||
req.headers_mut().append(
|
||||
"Auth-API-Key",
|
||||
"Auth-API-Token",
|
||||
HeaderValue::from_str(self.token.as_str()).unwrap(),
|
||||
);
|
||||
if let Some(payload) = payload {
|
||||
*req.body_mut() = Some(serde_json::to_string(&payload).unwrap().into());
|
||||
*req.body_mut() = Some(serde_json::to_string(&payload).map_err(|e| { println!("body encoding error: {}",e); ()} )?.into());
|
||||
}
|
||||
Ok(Client::new()
|
||||
let t = self.client
|
||||
.execute(req)
|
||||
.await
|
||||
.map_err(|_| ())?
|
||||
.json::<T>()
|
||||
.await
|
||||
.map_err(|_| ())?)
|
||||
.map_err(|e| {
|
||||
println!("request execution error: {}", e);
|
||||
()
|
||||
})?
|
||||
.error_for_status()
|
||||
.map_err(|e| { println!("request error: {}", e); ()})?
|
||||
.text().await
|
||||
.map_err(|e| { println!("request decoding error: {}", e); ()})?;
|
||||
Ok(serde_json::from_str::<T>(&t).map_err(|e| {println!("json response parsing error: {}",e); ()})?)
|
||||
}
|
||||
|
||||
async fn get_zones<'a>(
|
||||
pub async fn get_zones<'a>(
|
||||
&self,
|
||||
name: Option<&'a str>,
|
||||
page: Option<u32>,
|
||||
|
@ -125,7 +136,7 @@ impl HetznerDNSAPIClient {
|
|||
search_name: Option<&'a str>,
|
||||
) -> Result<ZoneResult, ()> {
|
||||
self.api_call(
|
||||
"/zones",
|
||||
"zones",
|
||||
Method::GET,
|
||||
Some(&[
|
||||
("name", name.unwrap_or_default()),
|
||||
|
@ -138,19 +149,19 @@ impl HetznerDNSAPIClient {
|
|||
.await
|
||||
}
|
||||
|
||||
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, ()> {
|
||||
self.api_call(
|
||||
"/zones",
|
||||
"zones",
|
||||
Method::POST,
|
||||
None::<&[(&str, &str); 0]>,
|
||||
Some(&[("name", name), ("ttl", ttl.unwrap_or(u64::MAX).to_string())]),
|
||||
Some(&[("name", name), ("ttl", ttl.unwrap_or(86400).to_string())]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_zone(&self, id: String) -> Result<Zone, ()> {
|
||||
pub async fn get_zone(&self, id: String) -> Result<Zone, ()> {
|
||||
self.api_call(
|
||||
format!("/zones/{}", id).as_str(),
|
||||
format!("zones/{}", id).as_str(),
|
||||
Method::GET,
|
||||
None::<[(&str, &str); 0]>,
|
||||
None::<&str>,
|
||||
|
@ -158,19 +169,24 @@ impl HetznerDNSAPIClient {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn update_zone(&self, id: String, name: String, ttl: Option<u64>) -> Result<Zone, ()> {
|
||||
pub async fn update_zone(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
ttl: Option<u64>,
|
||||
) -> Result<Zone, ()> {
|
||||
self.api_call(
|
||||
format!("/zones/{}", id).as_str(),
|
||||
format!("zones/{}", id).as_str(),
|
||||
Method::PUT,
|
||||
None::<[(&str, &str); 0]>,
|
||||
Some(&[("name", name), ("ttl", ttl.unwrap_or(u64::MAX).to_string())]),
|
||||
Some(&[("name", name), ("ttl", ttl.unwrap_or(86400).to_string())]),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_zone(&self, id: String) -> Result<(), ()> {
|
||||
pub async fn delete_zone(&self, id: String) -> Result<(), ()> {
|
||||
self.api_call(
|
||||
format!("/zones/{}", id).as_str(),
|
||||
format!("zones/{}", id).as_str(),
|
||||
Method::DELETE,
|
||||
None::<&[(&str, &str); 0]>,
|
||||
None::<&str>,
|
||||
|
@ -178,17 +194,17 @@ impl HetznerDNSAPIClient {
|
|||
.await?
|
||||
}
|
||||
|
||||
async fn import_zone() {
|
||||
pub async fn import_zone() {
|
||||
todo!()
|
||||
}
|
||||
async fn export_zone() {
|
||||
pub async fn export_zone() {
|
||||
todo!()
|
||||
}
|
||||
async fn validate_zone() {
|
||||
pub async fn validate_zone() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_records(
|
||||
pub async fn get_records(
|
||||
&self,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
|
@ -196,7 +212,7 @@ impl HetznerDNSAPIClient {
|
|||
) -> Result<Vec<Record>, ()> {
|
||||
let result: RecordsResult = self
|
||||
.api_call(
|
||||
"/records",
|
||||
"records",
|
||||
Method::GET,
|
||||
Some(&[
|
||||
("page", page.unwrap_or(1).to_string()),
|
||||
|
@ -210,10 +226,10 @@ impl HetznerDNSAPIClient {
|
|||
Ok(result.records)
|
||||
}
|
||||
|
||||
async fn create_record(&self, payload: RecordPayload) -> Result<Record, ()> {
|
||||
pub async fn create_record(&self, payload: RecordPayload) -> Result<Record, ()> {
|
||||
let result: RecordResult = self
|
||||
.api_call(
|
||||
"/records",
|
||||
"records",
|
||||
Method::POST,
|
||||
None::<[(&str, &str); 0]>,
|
||||
Some(payload),
|
||||
|
@ -223,10 +239,10 @@ impl HetznerDNSAPIClient {
|
|||
Ok(result.record)
|
||||
}
|
||||
|
||||
async fn get_record(&self, record_id: String) -> Result<Record, ()> {
|
||||
pub async fn get_record(&self, record_id: String) -> Result<Record, ()> {
|
||||
let result: RecordResult = self
|
||||
.api_call(
|
||||
format!("/records/{}", record_id).as_str(),
|
||||
format!("records/{}", record_id).as_str(),
|
||||
Method::GET,
|
||||
None::<[(&str, &str); 0]>,
|
||||
None::<u8>,
|
||||
|
@ -236,10 +252,14 @@ impl HetznerDNSAPIClient {
|
|||
Ok(result.record)
|
||||
}
|
||||
|
||||
async fn update_record(&self, record_id: String, payload: RecordPayload) -> Result<Record, ()> {
|
||||
pub async fn update_record(
|
||||
&self,
|
||||
record_id: String,
|
||||
payload: RecordPayload,
|
||||
) -> Result<Record, ()> {
|
||||
let result: RecordResult = self
|
||||
.api_call(
|
||||
format!("/records/{}", record_id).as_str(),
|
||||
format!("records/{}", record_id).as_str(),
|
||||
Method::PUT,
|
||||
None::<[(&str, &str); 0]>,
|
||||
Some(payload),
|
||||
|
@ -249,9 +269,9 @@ impl HetznerDNSAPIClient {
|
|||
Ok(result.record)
|
||||
}
|
||||
|
||||
async fn delete_record(&self, record_id: String) -> Result<(), ()> {
|
||||
pub async fn delete_record(&self, record_id: String) -> Result<(), ()> {
|
||||
self.api_call(
|
||||
format!("/records/{}", record_id).as_str(),
|
||||
format!("records/{}", record_id).as_str(),
|
||||
Method::DELETE,
|
||||
None::<[(&str, &str); 0]>,
|
||||
None::<u8>,
|
||||
|
@ -259,10 +279,10 @@ impl HetznerDNSAPIClient {
|
|||
.await?
|
||||
}
|
||||
|
||||
async fn create_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||
pub async fn create_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||
let result: RecordsResult = self
|
||||
.api_call(
|
||||
"/records/bulk",
|
||||
"records/bulk",
|
||||
Method::POST,
|
||||
None::<[(&str, &str); 0]>,
|
||||
Some(payloads),
|
||||
|
@ -272,10 +292,10 @@ impl HetznerDNSAPIClient {
|
|||
Ok(result.records)
|
||||
}
|
||||
|
||||
async fn update_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||
pub async fn update_records(&self, payloads: Vec<RecordPayload>) -> Result<Vec<Record>, ()> {
|
||||
let result: RecordsResult = self
|
||||
.api_call(
|
||||
"/records/bulk",
|
||||
"records/bulk",
|
||||
Method::PUT,
|
||||
None::<[(&str, &str); 0]>,
|
||||
Some(payloads),
|
||||
|
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#![allow(dead_code, unused)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::*;
|
||||
use std::error::Error;
|
||||
|
||||
pub mod models;
|
||||
pub mod client;
|
||||
|
191
src/main.rs
191
src/main.rs
|
@ -1,12 +1,193 @@
|
|||
#![allow(dead_code, unused)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use core::panic;
|
||||
use std::ops::Sub;
|
||||
use crate::client::*;
|
||||
use crate::models::*;
|
||||
use std::error::Error;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
mod models;
|
||||
mod client;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> () {
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
enum Mode {
|
||||
Unset,
|
||||
Errornous,
|
||||
Zone,
|
||||
Record,
|
||||
PrimaryServer,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
enum SubMode {
|
||||
Help,
|
||||
Get,
|
||||
Create,
|
||||
Update,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ZoneContext {
|
||||
all: bool,
|
||||
zone: String,
|
||||
name: String,
|
||||
ttl: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecordProto<'a> {
|
||||
r#type: RecordType,
|
||||
name: &'a str,
|
||||
value: &'a str,
|
||||
ttl: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecordContext<'a> {
|
||||
all: bool,
|
||||
zone: &'a str,
|
||||
records: Vec<RecordProto<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Context<'a> {
|
||||
mode: Mode,
|
||||
submode: SubMode,
|
||||
token: Option<String>,
|
||||
zone_context: ZoneContext,
|
||||
record_context: RecordContext<'a>,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let mut ctx = Context {
|
||||
mode: Mode::Unset,
|
||||
submode: SubMode::Help,
|
||||
token: std::env::var("HETZNER_DNS_API_TOKEN").ok(),
|
||||
zone_context: ZoneContext {
|
||||
all: false,
|
||||
zone: String::new(),
|
||||
name: String::new(),
|
||||
ttl: 86400,
|
||||
},
|
||||
record_context: RecordContext {
|
||||
all: false,
|
||||
zone: "",
|
||||
records: vec![],
|
||||
},
|
||||
};
|
||||
let mut _continue = false;
|
||||
for (idx, arg) in std::env::args().enumerate() {
|
||||
if _continue {
|
||||
continue;
|
||||
}
|
||||
match idx {
|
||||
0 => continue,
|
||||
1 => {
|
||||
ctx.mode = match arg.as_str() {
|
||||
"zones" => Mode::Zone,
|
||||
"records" => Mode::Record,
|
||||
"primary" => Mode::PrimaryServer,
|
||||
"z" => Mode::Zone,
|
||||
"r" => Mode::Record,
|
||||
"p" => Mode::PrimaryServer,
|
||||
_ => Mode::Errornous,
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
ctx.submode = match arg.as_str() {
|
||||
"get" => SubMode::Get,
|
||||
"create" => SubMode::Create,
|
||||
"update" => SubMode::Update,
|
||||
"delete" => SubMode::Delete,
|
||||
"g" => SubMode::Get,
|
||||
"c" => SubMode::Create,
|
||||
"u" => SubMode::Update,
|
||||
"d" => SubMode::Delete,
|
||||
_ => SubMode::Help,
|
||||
}
|
||||
}
|
||||
_ => match ctx.mode {
|
||||
Mode::Zone => {
|
||||
if arg.starts_with("-") {
|
||||
match arg.as_str() {
|
||||
"--all" => ctx.zone_context.all = true,
|
||||
"--name" => {
|
||||
ctx.zone_context.name = std::env::args().nth(idx + 1).unwrap()
|
||||
}
|
||||
"--ttl" => {
|
||||
ctx.zone_context.ttl = std::env::args()
|
||||
.nth(idx + 1)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap_or(86400)
|
||||
}
|
||||
"--zone" => {
|
||||
ctx.zone_context.zone = std::env::args().nth(idx + 1).unwrap()
|
||||
}
|
||||
_ => panic!("unknown parameter {}", arg),
|
||||
}
|
||||
} else {
|
||||
continue; // value, ignore
|
||||
}
|
||||
}
|
||||
Mode::Record => {
|
||||
if arg.starts_with("-") {
|
||||
match arg.as_str() {
|
||||
"--all" => ctx.record_context.all = true,
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::PrimaryServer => {
|
||||
todo!()
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.mode != Mode::Unset && ctx.mode != Mode::Errornous && ctx.submode != SubMode::Help {
|
||||
if let Some(ref token) = ctx.token {
|
||||
let client = HetznerDNSAPIClient::new(String::from(token.trim()));
|
||||
match ctx.mode {
|
||||
Mode::Zone => match ctx.submode {
|
||||
SubMode::Get => {
|
||||
if ctx.zone_context.all {
|
||||
println!(
|
||||
"{:#?}",
|
||||
client.get_zones(None, None, None, None).await.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
},
|
||||
Mode::Record => match ctx.submode {
|
||||
SubMode::Get => {
|
||||
if ctx.record_context.all {
|
||||
println!("{:#?}", client.get_records(None, None, None).await.unwrap());
|
||||
}
|
||||
}
|
||||
_ => { todo!() }
|
||||
}
|
||||
Mode::PrimaryServer => {
|
||||
todo!()
|
||||
}
|
||||
_ => panic!("how in the even"),
|
||||
}
|
||||
} else {
|
||||
panic!("missing token!");
|
||||
}
|
||||
} else {
|
||||
match ctx.mode {
|
||||
Mode::Zone => println!("zone help"),
|
||||
Mode::Record => println!("record help"),
|
||||
Mode::PrimaryServer => println!("primary server help"),
|
||||
_ => println!("full help"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,12 @@ pub struct Meta {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub struct Zone {
|
||||
pub id: String,
|
||||
#[serde(with = "hetzner_date")]
|
||||
pub created: DateTime<Utc>,
|
||||
#[serde(with = "hetzner_date")]
|
||||
pub modified: DateTime<Utc>,
|
||||
pub legacy_dns_host: String,
|
||||
pub legacy_dns: Vec<String>,
|
||||
pub legacy_dns: Option<Vec<String>>,
|
||||
pub ns: Vec<String>,
|
||||
pub owner: String,
|
||||
pub paused: bool,
|
||||
|
@ -35,11 +37,12 @@ pub struct Zone {
|
|||
pub project: String,
|
||||
pub registrar: String,
|
||||
pub status: String,
|
||||
pub ttl: u32,
|
||||
pub verified: DateTime<Utc>,
|
||||
pub ttl: Option<u64>,
|
||||
//#[serde(with = "hetzner_date")] // verified strings are empty, so its useless anyway
|
||||
//pub verified: Option<DateTime<Utc>>,
|
||||
pub records_count: u32,
|
||||
pub is_secondary_dns: bool,
|
||||
pub txt_verification: Vec<TxtVerification>
|
||||
pub txt_verification: TxtVerification
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -60,13 +63,15 @@ pub struct RecordPayload {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Record {
|
||||
id: String,
|
||||
#[serde(with = "hetzner_date")]
|
||||
created: DateTime<Utc>,
|
||||
#[serde(with = "hetzner_date")]
|
||||
modified: DateTime<Utc>,
|
||||
zone_id: String,
|
||||
r#type: String,
|
||||
name: String,
|
||||
value: String,
|
||||
ttl: u64
|
||||
ttl: Option<u64>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -78,3 +83,33 @@ pub struct RecordResult {
|
|||
pub struct RecordsResult {
|
||||
pub records: Vec<Record>
|
||||
}
|
||||
|
||||
mod hetzner_date {
|
||||
use chrono::{DateTime, Utc, NaiveDateTime};
|
||||
use serde::{self, Deserialize, Serializer, Deserializer};
|
||||
|
||||
// 2025-01-06 02:18:34.674 +0000 UTC
|
||||
const FORMAT: &str = "%F %T.%-f";
|
||||
|
||||
pub fn serialize<S>(
|
||||
date: &DateTime<Utc>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let s = format!("{}", date.format(FORMAT));
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<DateTime<Utc>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let dt = NaiveDateTime::parse_from_str(&s.split(" +").next().unwrap(), FORMAT).map_err(serde::de::Error::custom)?;
|
||||
Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue