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