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