zone_update/linode/
mod.rs1mod types;
2
3use std::{fmt::{Debug, Display}, sync::Mutex};
4
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use tracing::{error, info, warn};
7
8use crate::{
9 errors::{Error, Result}, generate_helpers, http::{self, ResponseToOption, WithHeaders}, linode::types::{CreateUpdate, Domain, List, Record}, Config, DnsProvider, RecordType
10};
11
12const API_BASE: &str = "https://api.linode.com/v4/domains";
13
14#[derive(Clone, Debug, Deserialize)]
18pub struct Auth {
19 pub key: String,
20}
21
22impl Auth {
23 fn get_header(&self) -> String {
24 format!("Bearer {}", self.key)
25 }
26}
27
28
29pub struct Linode {
33 config: Config,
34 auth: Auth,
35 domain_id: Mutex<Option<u64>>,
36}
37
38impl Linode {
39 pub fn new(config: Config, auth: Auth) -> Self {
41 Self {
42 config,
43 auth,
44 domain_id: Mutex::new(None),
45 }
46 }
47
48 fn get_domain(&self) -> Result<Domain> {
49 let list = http::client().get(API_BASE)
50 .with_auth(self.auth.get_header())
51 .with_json_headers()
52 .call()?
53 .to_option::<List<Domain>>()?
54 .ok_or(Error::ApiError("No domains returned from upstream".to_string()))?;
55
56 let domain = list.data.into_iter()
57 .find(|d| d.domain == self.config.domain)
58 .ok_or(Error::RecordNotFound(self.config.domain.clone()))?;
59
60 Ok(domain)
61 }
62
63 fn get_domain_id(&self) -> Result<u64> {
64 let mut id_p = self.domain_id.lock()
68 .map_err(|e| Error::LockingError(e.to_string()))?;
69
70 if let Some(id) = *id_p {
71 return Ok(id);
72 }
73
74 let id = self.get_domain()?.id;
75 *id_p = Some(id);
76
77 Ok(id)
78 }
79
80 fn get_upstream_records<T>(&self, rtype: &RecordType, host: &str) -> Result<Vec<Record<T>>>
81 where T: DeserializeOwned
82 {
83 let did = self.get_domain_id()?;
84 let url = format!("{API_BASE}/{did}/records");
85
86 let mut response = http::client().get(url)
87 .with_auth(self.auth.get_header())
88 .with_json_headers()
89 .call()?;
90
91 let body = response.body_mut().read_to_string()?;
96 let srtype = rtype.to_string();
97
98 let values: serde_json::Value = serde_json::from_str(&body)?;
99 let data = values["data"].as_array()
100 .ok_or(Error::ApiError("Data field not found".to_string()))?;
101 let records = data.iter()
102 .filter_map(|obj| match &obj["type"] {
103 serde_json::Value::String(t)
104 if t == &srtype && obj["name"] == host
105 => Some(serde_json::from_value(obj.clone())),
106 _ => None,
107 })
108 .collect::<std::result::Result<Vec<Record<T>>, _>>()?;
109
110 Ok(records)
111 }
112
113 fn get_upstream_record<T>(&self, rtype: &RecordType, host: &str) -> Result<Option<Record<T>>>
114 where T: DeserializeOwned
115 {
116 let mut recs = self.get_upstream_records(rtype, host)?;
117
118 let nr = recs.len();
122 if nr > 1 {
123 error!("Returned number of records 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 record returned for {host}, continuing");
127 return Ok(None);
128 }
129
130 Ok(Some(recs.remove(0)))
131 }
132
133 fn do_delete(&self, rec: Record<String>) -> Result<()> {
134 let did = self.get_domain_id()?;
135 let url = format!("{API_BASE}/{did}/records/{}", rec.id);
136 if self.config.dry_run {
137 info!("DRY-RUN: Would have sent DELETE to {url}");
138 return Ok(())
139 }
140
141 info!("Deleting DNS {} record {}", rec.rtype, rec.name);
142 http::client().delete(url)
143 .with_auth(self.auth.get_header())
144 .with_json_headers()
145 .call()?;
146
147 Ok(())
148 }
149}
150
151
152impl DnsProvider for Linode {
153
154 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
155 where
156 T: DeserializeOwned
157 {
158 let rec = match self.get_upstream_record(&rtype, host)? {
159 Some(rec) => rec,
160 None => return Ok(None)
161 };
162
163 Ok(Some(rec.target))
164 }
165
166 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
167 where
168 T: Serialize + DeserializeOwned + Display + Clone
169 {
170 let did = self.get_domain_id()?;
171 let url = format!("{API_BASE}/{did}/records");
172
173 let create = CreateUpdate {
174 name: host.to_string(),
175 rtype,
176 target: record.to_string(),
177 ttl_sec: 300,
178 };
179 if self.config.dry_run {
180 info!("DRY-RUN: Would have sent {create:?} to {url}");
181 return Ok(())
182 }
183
184 let body = serde_json::to_string(&create)?;
185 let _response = http::client().post(url)
186 .with_auth(self.auth.get_header())
187 .with_json_headers()
188 .send(body)?
189 .check_error()?;
190
191 Ok(())
192 }
193
194 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
195 where
196 T: Serialize + DeserializeOwned + Display + Clone
197 {
198 let did = self.get_domain_id()?;
199 let rec: Record<T> = self.get_upstream_record(&rtype, host)?
200 .ok_or(Error::RecordNotFound(host.to_string()))?;
201 let url = format!("{API_BASE}/{did}/records/{}", rec.id);
202
203 let update = CreateUpdate {
204 name: host.to_string(),
205 rtype,
206 target: urec.to_string(),
207 ttl_sec: 300,
208 };
209
210 if self.config.dry_run {
211 info!("DRY-RUN: Would have sent {update:?} to {url}");
212 return Ok(())
213 }
214
215 let body = serde_json::to_string(&update)?;
216 let _response = http::client().put(url)
217 .with_auth(self.auth.get_header())
218 .with_json_headers()
219 .send(body)?
220 .check_error()?;
221
222 Ok(())
223 }
224
225 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
226 {
227 let rec = match self.get_upstream_record(&rtype, host)? {
228 Some(rec) => rec,
229 None => {
230 warn!("No {rtype} record to delete for {host}");
231 return Ok(());
232 }
233 };
234
235 self.do_delete(rec)
236 }
237
238 fn delete_all_records(&self, rtype: RecordType, host: &str) -> Result<()>
239 {
240 let recs: Vec<Record<String>> = self.get_upstream_records(&rtype, host)?;
241 for rec in recs {
242 self.do_delete(rec)?;
243 }
244
245 Ok(())
246 }
247
248 generate_helpers!();
249
250}
251
252
253#[cfg(test)]
254pub(crate) mod tests {
255 use super::*;
256 use crate::{generate_tests, tests::*};
257 use std::env;
258
259 fn get_client() -> Linode {
260 let auth = Auth {
261 key: env::var("LINODE_API_KEY").unwrap(),
262 };
263 let config = Config {
264 domain: env::var("LINODE_TEST_DOMAIN").unwrap(),
265 dry_run: false,
266 };
267 Linode::new(config, auth)
268 }
269
270 generate_tests!("test_linode");
271}