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