dns_update/providers/
gcore.rs1use crate::{DnsRecord, DnsRecordType, Error, IntoFqdn, http::HttpClientBuilder};
13use serde::{Deserialize, Serialize};
14use std::{borrow::Cow, time::Duration};
15
16const DEFAULT_API_ENDPOINT: &str = "https://api.gcore.com/dns";
17
18#[derive(Clone)]
19pub struct GcoreProvider {
20 client: HttpClientBuilder,
21 endpoint: Cow<'static, str>,
22}
23
24#[derive(Deserialize, Debug)]
25struct Zone {
26 name: String,
27}
28
29#[derive(Serialize, Debug)]
30struct RrSet {
31 ttl: u32,
32 resource_records: Vec<ResourceRecord>,
33}
34
35#[derive(Serialize, Debug)]
36struct ResourceRecord {
37 content: Vec<serde_json::Value>,
38}
39
40impl GcoreProvider {
41 pub(crate) fn new(api_token: impl AsRef<str>, timeout: Option<Duration>) -> Self {
42 let client = HttpClientBuilder::default()
43 .with_header("Authorization", format!("APIKey {}", api_token.as_ref()))
44 .with_timeout(timeout);
45 Self {
46 client,
47 endpoint: Cow::Borrowed(DEFAULT_API_ENDPOINT),
48 }
49 }
50
51 #[cfg(test)]
52 pub(crate) fn with_endpoint(self, endpoint: impl Into<Cow<'static, str>>) -> Self {
53 Self {
54 endpoint: endpoint.into(),
55 ..self
56 }
57 }
58
59 pub(crate) async fn create(
60 &self,
61 name: impl IntoFqdn<'_>,
62 record: DnsRecord,
63 ttl: u32,
64 origin: impl IntoFqdn<'_>,
65 ) -> crate::Result<()> {
66 let zone = self.obtain_zone(&origin.into_name()).await?;
67 let fqdn = name.into_name();
68 let record_type = record.as_type().as_str();
69 let body = build_rrset(record, ttl)?;
70 self.client
71 .post(format!(
72 "{}/v2/zones/{}/{}/{}",
73 self.endpoint, zone, fqdn, record_type
74 ))
75 .with_body(body)?
76 .send_raw()
77 .await
78 .map(|_| ())
79 }
80
81 pub(crate) async fn update(
82 &self,
83 name: impl IntoFqdn<'_>,
84 record: DnsRecord,
85 ttl: u32,
86 origin: impl IntoFqdn<'_>,
87 ) -> crate::Result<()> {
88 let zone = self.obtain_zone(&origin.into_name()).await?;
89 let fqdn = name.into_name();
90 let record_type = record.as_type().as_str();
91 let body = build_rrset(record, ttl)?;
92 self.client
93 .put(format!(
94 "{}/v2/zones/{}/{}/{}",
95 self.endpoint, zone, fqdn, record_type
96 ))
97 .with_body(body)?
98 .send_raw()
99 .await
100 .map(|_| ())
101 }
102
103 pub(crate) async fn delete(
104 &self,
105 name: impl IntoFqdn<'_>,
106 origin: impl IntoFqdn<'_>,
107 record_type: DnsRecordType,
108 ) -> crate::Result<()> {
109 let zone = self.obtain_zone(&origin.into_name()).await?;
110 let fqdn = name.into_name();
111 self.client
112 .delete(format!(
113 "{}/v2/zones/{}/{}/{}",
114 self.endpoint,
115 zone,
116 fqdn,
117 record_type.as_str()
118 ))
119 .send_raw()
120 .await
121 .map(|_| ())
122 }
123
124 async fn obtain_zone(&self, origin: &str) -> crate::Result<String> {
125 let mut candidate: &str = origin;
126 loop {
127 let result = self
128 .client
129 .get(format!("{}/v2/zones/{}", self.endpoint, candidate))
130 .send_with_retry::<Zone>(3)
131 .await;
132 match result {
133 Ok(zone) => return Ok(zone.name),
134 Err(Error::NotFound) => {}
135 Err(err) => return Err(err),
136 }
137 match candidate.split_once('.') {
138 Some((_, rest)) if rest.contains('.') => candidate = rest,
139 _ => {
140 return Err(Error::Api(format!("No Gcore zone found for {origin}")));
141 }
142 }
143 }
144 }
145}
146
147fn build_rrset(record: DnsRecord, ttl: u32) -> crate::Result<RrSet> {
148 use serde_json::Value;
149 let content: Vec<Value> = match record {
150 DnsRecord::A(addr) => vec![Value::String(addr.to_string())],
151 DnsRecord::AAAA(addr) => vec![Value::String(addr.to_string())],
152 DnsRecord::CNAME(content) => vec![Value::String(content)],
153 DnsRecord::NS(content) => vec![Value::String(content)],
154 DnsRecord::MX(mx) => vec![
155 Value::Number(mx.priority.into()),
156 Value::String(mx.exchange),
157 ],
158 DnsRecord::TXT(content) => vec![Value::String(content)],
159 DnsRecord::SRV(srv) => vec![
160 Value::Number(srv.priority.into()),
161 Value::Number(srv.weight.into()),
162 Value::Number(srv.port.into()),
163 Value::String(srv.target),
164 ],
165 DnsRecord::TLSA(tlsa) => vec![Value::String(tlsa.to_string())],
166 DnsRecord::CAA(caa) => {
167 let (flags, tag, value) = caa.decompose();
168 vec![
169 Value::Number(flags.into()),
170 Value::String(tag),
171 Value::String(value),
172 ]
173 }
174 };
175 Ok(RrSet {
176 ttl,
177 resource_records: vec![ResourceRecord { content }],
178 })
179}