use std::error::Error;
use reqwest::{
header::{HeaderMap, HeaderValue, AUTHORIZATION},
Client as HttpClient,
};
use serde::{Deserialize, Serialize};
use crate::HttpClientConfig;
const HETZNER_API_URL: &str = "https://api.hetzner.cloud/v1";
#[derive(Debug, Clone)]
pub struct Client {
http_client: HttpClient,
base_url: String,
}
impl Client {
pub fn new(api_key: &str) -> Result<Self, Box<dyn Error>> {
Self::with_base_url_and_config(api_key, HETZNER_API_URL, HttpClientConfig::default())
}
pub fn with_config(api_key: &str, config: HttpClientConfig) -> Result<Self, Box<dyn Error>> {
Self::with_base_url_and_config(api_key, HETZNER_API_URL, config)
}
pub fn with_base_url(api_key: &str, base_url: &str) -> Result<Self, Box<dyn Error>> {
Self::with_base_url_and_config(api_key, base_url, HttpClientConfig::default())
}
pub fn with_base_url_and_config(
api_key: &str,
base_url: &str,
config: HttpClientConfig,
) -> Result<Self, Box<dyn Error>> {
let mut headers = HeaderMap::new();
let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", api_key))?;
auth_value.set_sensitive(true);
headers.append(AUTHORIZATION, auth_value);
let mut builder = HttpClient::builder().default_headers(headers);
if let Some(timeout) = config.timeout {
builder = builder.timeout(timeout);
}
if let Some(addr) = config.local_address {
builder = builder.local_address(addr);
}
#[cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "linux",
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos",
target_os = "illumos",
target_os = "solaris",
))]
if let Some(ref iface) = config.interface {
builder = builder.interface(iface);
}
let http_client = builder.build()?;
Ok(Self {
http_client,
base_url: base_url.to_string(),
})
}
pub async fn retrieve_zones(
&self,
page: u32,
per_page: u32,
) -> Result<ZonesResponse, reqwest::Error> {
self.http_client
.get(format!(
"{}/zones?page={}&per_page={}",
self.base_url, page, per_page
))
.send()
.await?
.json::<ZonesResponse>()
.await
}
pub async fn retrieve_zone(
&self,
zone_id_or_name: &str,
) -> Result<ZoneResponse, reqwest::Error> {
self.http_client
.get(format!("{}/zones/{}", self.base_url, zone_id_or_name))
.send()
.await?
.json()
.await
}
pub async fn create_zone(
&self,
domain: &str,
ttl: Option<u64>,
) -> Result<CreateZoneResponse, reqwest::Error> {
let request_body = CreateZoneRequest {
name: domain.to_string(),
mode: "primary".to_string(),
ttl: ttl.unwrap_or(3600),
};
self.http_client
.post(format!("{}/zones", self.base_url))
.json(&request_body)
.send()
.await?
.json()
.await
}
pub async fn delete_zone(&self, zone_id_or_name: &str) -> Result<(), reqwest::Error> {
self.http_client
.delete(format!("{}/zones/{}", self.base_url, zone_id_or_name))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn retrieve_rrsets(
&self,
zone_id_or_name: &str,
page: u32,
per_page: u32,
) -> Result<RRSetsResponse, reqwest::Error> {
self.http_client
.get(format!(
"{}/zones/{}/rrsets?page={}&per_page={}",
self.base_url, zone_id_or_name, page, per_page
))
.send()
.await?
.json()
.await
}
pub async fn retrieve_rrset(
&self,
zone_id_or_name: &str,
rr_name: &str,
rr_type: &str,
) -> Result<RRSetResponse, reqwest::Error> {
self.http_client
.get(format!(
"{}/zones/{}/rrsets/{}/{}",
self.base_url, zone_id_or_name, rr_name, rr_type
))
.send()
.await?
.json()
.await
}
pub async fn create_rrset(
&self,
zone_id_or_name: &str,
name: &str,
typ: &str,
records: Vec<RecordValue>,
ttl: Option<u64>,
) -> Result<CreateRRSetResponse, reqwest::Error> {
let request_body = CreateRRSetRequest {
name: name.to_string(),
typ: typ.to_string(),
records,
ttl,
};
self.http_client
.post(format!(
"{}/zones/{}/rrsets",
self.base_url, zone_id_or_name
))
.json(&request_body)
.send()
.await?
.json()
.await
}
pub async fn add_records_to_rrset(
&self,
zone_id_or_name: &str,
rr_name: &str,
rr_type: &str,
records: Vec<RecordValue>,
ttl: Option<u64>,
) -> Result<ActionResponse, reqwest::Error> {
let request_body = AddRecordsRequest { records, ttl };
self.http_client
.post(format!(
"{}/zones/{}/rrsets/{}/{}/actions/add_records",
self.base_url, zone_id_or_name, rr_name, rr_type
))
.json(&request_body)
.send()
.await?
.json()
.await
}
pub async fn remove_records_from_rrset(
&self,
zone_id_or_name: &str,
rr_name: &str,
rr_type: &str,
records: Vec<RecordValue>,
) -> Result<ActionResponse, reqwest::Error> {
let request_body = RemoveRecordsRequest { records };
self.http_client
.post(format!(
"{}/zones/{}/rrsets/{}/{}/actions/remove_records",
self.base_url, zone_id_or_name, rr_name, rr_type
))
.json(&request_body)
.send()
.await?
.json()
.await
}
pub async fn delete_rrset(
&self,
zone_id_or_name: &str,
rr_name: &str,
rr_type: &str,
) -> Result<ActionResponse, reqwest::Error> {
self.http_client
.delete(format!(
"{}/zones/{}/rrsets/{}/{}",
self.base_url, zone_id_or_name, rr_name, rr_type
))
.send()
.await?
.json()
.await
}
}
#[derive(Debug, Serialize)]
struct CreateZoneRequest {
name: String,
mode: String,
ttl: u64,
}
#[derive(Debug, Serialize)]
struct CreateRRSetRequest {
name: String,
#[serde(rename = "type")]
typ: String,
records: Vec<RecordValue>,
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<u64>,
}
#[derive(Debug, Serialize)]
struct AddRecordsRequest {
records: Vec<RecordValue>,
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<u64>,
}
#[derive(Debug, Serialize)]
struct RemoveRecordsRequest {
records: Vec<RecordValue>,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct Zone {
pub id: u64,
pub name: String,
pub mode: String,
pub status: ZoneStatus,
pub ttl: u64,
#[serde(default)]
pub record_count: u32,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ZoneStatus {
Ok,
Pending,
Failed,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct ZoneResponse {
pub zone: Zone,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct CreateZoneResponse {
pub zone: Zone,
pub action: Action,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct ZonesResponse {
pub meta: Meta,
pub zones: Vec<Zone>,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct RRSet {
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub typ: String,
pub ttl: Option<u64>,
pub records: Vec<RecordValue>,
pub zone: u64,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct RecordValue {
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
impl RecordValue {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
comment: None,
}
}
pub fn with_comment(value: impl Into<String>, comment: impl Into<String>) -> Self {
Self {
value: value.into(),
comment: Some(comment.into()),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct RRSetResponse {
pub rrset: RRSet,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct CreateRRSetResponse {
pub rrset: RRSet,
pub action: Action,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct RRSetsResponse {
pub meta: Meta,
pub rrsets: Vec<RRSet>,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct ActionResponse {
pub action: Action,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct Action {
pub id: u64,
pub command: String,
pub status: ActionStatus,
pub progress: u32,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActionStatus {
Running,
Success,
Error,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct Meta {
pub pagination: Pagination,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize)]
pub struct Pagination {
pub last_page: u32,
pub page: u32,
pub per_page: u32,
pub total_entries: u32,
}