1mod types;
2
3use std::{fmt::Display, sync::Mutex};
4
5use serde::de::DeserializeOwned;
6use serde::Deserialize;
7use tracing::{error, info, warn};
8
9use crate::generate_helpers;
10use crate::http::{self, ResponseToOption, WithHeaders};
11
12
13use crate::{
14 dnsimple::types::{
15 Accounts,
16 CreateRecord,
17 GetRecord,
18 Records,
19 UpdateRecord
20 },
21 errors::{Error, Result},
22 Config,
23 DnsProvider,
24 RecordType
25};
26
27
28pub(crate) const API_BASE: &str = "https://api.dnsimple.com/v2";
29
30#[derive(Clone, Debug, Deserialize)]
34pub struct Auth {
35 pub key: String,
36}
37
38impl Auth {
39 fn get_header(&self) -> String {
40 format!("Bearer {}", self.key)
41 }
42}
43
44pub struct Dnsimple {
48 config: Config,
49 endpoint: &'static str,
50 auth: Auth,
51 acc_id: Mutex<Option<u32>>,
52}
53
54impl Dnsimple {
55 pub fn new(config: Config, auth: Auth, acc: Option<u32>) -> Self {
57 Self::new_with_endpoint(config, auth, acc, API_BASE)
58 }
59
60 pub fn new_with_endpoint(config: Config, auth: Auth, acc: Option<u32>, endpoint: &'static str) -> Self {
62 let acc_id = Mutex::new(acc);
63 Dnsimple {
64 config,
65 endpoint,
66 auth,
67 acc_id,
68 }
69 }
70
71 fn get_upstream_id(&self) -> Result<u32> {
72 info!("Fetching account ID from upstream");
73 let url = format!("{}/accounts", self.endpoint);
74
75 let accounts_p = http::client().get(url)
76 .with_auth(self.auth.get_header())
77 .call()?
78 .to_option::<Accounts>()?;
79
80 match accounts_p {
81 Some(accounts) if accounts.accounts.len() == 1 => {
82 Ok(accounts.accounts[0].id)
83 }
84 Some(accounts) if accounts.accounts.len() > 1 => {
85 Err(Error::ApiError("More than one account returned; you must specify the account ID to use".to_string()))
86 }
87 _ => {
89 Err(Error::ApiError("No accounts returned from upstream".to_string()))
90 }
91 }
92 }
93
94 fn get_id(&self) -> Result<u32> {
95 let mut id_p = self.acc_id.lock()
99 .map_err(|e| Error::LockingError(e.to_string()))?;
100
101 if let Some(id) = *id_p {
102 return Ok(id);
103 }
104
105 let id = self.get_upstream_id()?;
106 *id_p = Some(id);
107
108 Ok(id)
109 }
110
111 fn get_upstream_records<T>(&self, rtype: RecordType, host: &str) -> Result<Vec<GetRecord<T>>>
112 where
113 T: DeserializeOwned
114 {
115 let acc_id = self.get_id()?;
116 let url = format!("{}/{acc_id}/zones/{}/records?name={host}&type={rtype}", self.endpoint, self.config.domain);
117
118 let response = http::client().get(url)
119 .with_json_headers()
120 .with_auth(self.auth.get_header())
121 .call()?
122 .to_option::<Records<T>>()?;
123 let recs: Records<T> = match response {
124 Some(rec) => rec,
125 None => return Ok(vec![])
126 };
127
128 Ok(recs.records)
129 }
130
131 fn get_upstream_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<GetRecord<T>>>
132 where
133 T: DeserializeOwned
134 {
135 let mut recs = self.get_upstream_records(rtype, host)?;
136
137 let nr = recs.len();
141 if nr > 1 {
142 error!("Returned number of IPs is {}, should be 1", nr);
143 return Err(Error::UnexpectedRecord(format!("Returned number of records is {nr}, should be 1")));
144 } else if nr == 0 {
145 warn!("No IP returned for {host}, continuing");
146 return Ok(None);
147 }
148
149 Ok(Some(recs.remove(0)))
150 }
151
152 fn do_delete(&self, rec: GetRecord<String>) -> Result<()> {
153 let acc_id = self.get_id()?;
154 let url = format!("{}/{acc_id}/zones/{}/records/{}", self.endpoint, self.config.domain, rec.id);
155 if self.config.dry_run {
156 info!("DRY-RUN: Would have sent DELETE to {url}");
157 return Ok(())
158 }
159
160 info!("Deleting DNS {} record {}", rec.rtype, rec.name);
161 http::client().delete(url)
162 .with_json_headers()
163 .with_auth(self.auth.get_header())
164 .call()?;
165
166 Ok(())
167 }
168
169}
170
171
172impl DnsProvider for Dnsimple {
173
174 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T> >
175 where
176 T: DeserializeOwned
177 {
178 let rec: GetRecord<T> = match self.get_upstream_record(rtype, host)? {
179 Some(recs) => recs,
180 None => return Ok(None)
181 };
182
183
184 Ok(Some(rec.content))
185 }
186
187 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
188 where
189 T: Display,
190 {
191 let acc_id = self.get_id()?;
192
193 let url = format!("{}/{acc_id}/zones/{}/records", self.endpoint, self.config.domain);
194
195 let rec = CreateRecord {
196 name: host.to_string(),
197 rtype,
198 content: record.to_string(),
199 ttl: 300,
200 };
201
202 if self.config.dry_run {
203 info!("DRY-RUN: Would have sent {rec:?} to {url}");
204 return Ok(())
205 }
206
207 let body = serde_json::to_string(&rec)?;
208 http::client().post(url)
209 .with_json_headers()
210 .with_auth(self.auth.get_header())
211 .send(body)?;
212
213 Ok(())
214 }
215
216 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
217 where
218 T: DeserializeOwned + Display,
219 {
220 let rec: GetRecord<T> = match self.get_upstream_record(rtype, host)? {
221 Some(rec) => rec,
222 None => {
223 warn!("DELETE: Record {host} doesn't exist");
224 return Ok(());
225 }
226 };
227
228 let acc_id = self.get_id()?;
229 let rid = rec.id;
230
231 let update = UpdateRecord {
232 content: urec.to_string(),
233 };
234
235 let url = format!("{}/{acc_id}/zones/{}/records/{rid}", self.endpoint, self.config.domain);
236 if self.config.dry_run {
237 info!("DRY-RUN: Would have sent PATCH to {url}");
238 return Ok(())
239 }
240
241
242 let body = serde_json::to_string(&update)?;
243 http::client().patch(url)
244 .with_json_headers()
245 .with_auth(self.auth.get_header())
246 .send(body)?;
247
248 Ok(())
249 }
250
251 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()> {
252 let rec: GetRecord<String> = match self.get_upstream_record(rtype, host)? {
253 Some(rec) => rec,
254 None => {
255 warn!("DELETE: Record {host} doesn't exist");
256 return Ok(());
257 }
258 };
259
260 self.do_delete(rec)?;
261
262 Ok(())
263 }
264
265 fn delete_all_records(&self, rtype: RecordType, host: &str) -> Result<()>
266 {
267 let recs: Vec<GetRecord<String>> = self.get_upstream_records(rtype, host)?;
268 for rec in recs {
269 self.do_delete(rec)?;
270 }
271
272 Ok(())
273 }
274
275 generate_helpers!();
276}
277
278
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::{generate_tests, tests::*};
284 use std::env;
285
286 const TEST_API: &str = "https://api.sandbox.dnsimple.com/v2";
287
288 fn get_client() -> Dnsimple {
289 let auth = Auth { key: env::var("DNSIMPLE_TOKEN").unwrap() };
290 let config = Config {
291 domain: env::var("DNSIMPLE_TEST_DOMAIN").unwrap(),
292 dry_run: false,
293 };
294 Dnsimple::new_with_endpoint(config, auth, None, TEST_API)
295 }
296
297 #[test_log::test]
298 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "Dnsimple API test")]
299 fn test_id_fetch() -> Result<()> {
300 let client = get_client();
301
302 let id = client.get_upstream_id()?;
303 assert_eq!(2602, id);
304
305 Ok(())
306 }
307
308 generate_tests!("test_dnsimple");
309}