zone_update/linode/
mod.rs1mod types;
2
3use std::{fmt::{Debug, Display}, sync::Mutex};
4
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use tracing::{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: &'static 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 .filter(|d| d.domain == self.config.domain)
58 .next()
59 .ok_or(Error::RecordNotFound(self.config.domain.clone()))?;
60
61 Ok(domain)
62 }
63
64 fn get_domain_id(&self) -> Result<u64> {
65 let mut id_p = self.domain_id.lock()
69 .map_err(|e| Error::LockingError(e.to_string()))?;
70
71 if let Some(id) = *id_p {
72 return Ok(id);
73 }
74
75 let id = self.get_domain()?.id;
76 *id_p = Some(id);
77
78 Ok(id)
79 }
80
81 fn get_upstream_record<T>(&self, rtype: &RecordType, host: &str) -> Result<Option<Record<T>>>
82 where T: DeserializeOwned
83 {
84 let did = self.get_domain_id()?;
85 let url = format!("{API_BASE}/{did}/records");
86
87 let mut response = http::client().get(url)
88 .with_auth(self.auth.get_header())
89 .with_json_headers()
90 .call()?;
91
92 let body = response.body_mut().read_to_string()?;
97 let srtype = rtype.to_string();
98
99 let values: serde_json::Value = serde_json::from_str(&body)?;
100 let data = values["data"].as_array()
101 .ok_or(Error::ApiError("Data field not found".to_string()))?;
102 let record = data.into_iter()
103 .filter_map(|obj| match &obj["type"] {
104 serde_json::Value::String(t)
105 if t == &srtype && obj["name"] == host
106 => Some(serde_json::from_value(obj.clone())),
107 _ => None,
108 })
109 .next()
110 .transpose()?;
111
112 Ok(record)
113 }
114
115 fn get_record_id(&self, rtype: &RecordType, host: &str) -> Result<Option<u64>>
116 {
117 Ok(self.get_upstream_record::<String>(rtype, host)?
118 .map(|r| r.id))
119 }
120}
121
122
123impl DnsProvider for Linode {
124
125 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
126 where
127 T: DeserializeOwned
128 {
129 let rec = match self.get_upstream_record(&rtype, host)? {
130 Some(rec) => rec,
131 None => return Ok(None)
132 };
133
134 Ok(Some(rec.target))
135 }
136
137 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
138 where
139 T: Serialize + DeserializeOwned + Display + Clone
140 {
141 let did = self.get_domain_id()?;
142 let url = format!("{API_BASE}/{did}/records");
143
144 let create = CreateUpdate {
145 name: host.to_string(),
146 rtype,
147 target: record.to_string(),
148 ttl_sec: 300,
149 };
150 if self.config.dry_run {
151 info!("DRY-RUN: Would have sent {create:?} to {url}");
152 return Ok(())
153 }
154
155 let body = serde_json::to_string(&create)?;
156 let _response = http::client().post(url)
157 .with_auth(self.auth.get_header())
158 .with_json_headers()
159 .send(body)?
160 .check_error()?;
161
162 Ok(())
163 }
164
165 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
166 where
167 T: Serialize + DeserializeOwned + Display + Clone
168 {
169 let did = self.get_domain_id()?;
170 let id = self.get_record_id(&rtype, host)?
171 .ok_or(Error::RecordNotFound(host.to_string()))?;
172 let url = format!("{API_BASE}/{did}/records/{id}");
173
174 let update = CreateUpdate {
175 name: host.to_string(),
176 rtype,
177 target: urec.to_string(),
178 ttl_sec: 300,
179 };
180
181 if self.config.dry_run {
182 info!("DRY-RUN: Would have sent {update:?} to {url}");
183 return Ok(())
184 }
185
186 let body = serde_json::to_string(&update)?;
187 let _response = http::client().put(url)
188 .with_auth(self.auth.get_header())
189 .with_json_headers()
190 .send(body)?
191 .check_error()?;
192
193 Ok(())
194 }
195
196 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
197 {
198 let id = match self.get_record_id(&rtype, host)? {
199 Some(id) => id,
200 None => {
201 warn!("No {rtype} record to delete for {host}");
202 return Ok(());
203 }
204 };
205
206 let did = self.get_domain_id()?;
207 let url = format!("{API_BASE}/{did}/records/{id}");
208 if self.config.dry_run {
209 info!("DRY-RUN: Would have sent DELETE to {url}");
210 return Ok(())
211 }
212
213 http::client().delete(url)
214 .with_auth(self.auth.get_header())
215 .with_json_headers()
216 .call()?;
217
218 Ok(())
219 }
220
221 generate_helpers!();
222
223}
224
225
226#[cfg(test)]
227pub(crate) mod tests {
228 use super::*;
229 use crate::{generate_tests, tests::*};
230 use std::env;
231
232 fn get_client() -> Linode {
233 let auth = Auth {
234 key: env::var("LINODE_API_KEY").unwrap(),
235 };
236 let config = Config {
237 domain: env::var("LINODE_TEST_DOMAIN").unwrap(),
238 dry_run: false,
239 };
240 Linode::new(config, auth)
241 }
242
243 generate_tests!("test_linode");
244}