zone_update/digitalocean/
mod.rs1mod types;
2
3use std::fmt::Display;
4
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use tracing::{error, info, warn};
7
8use crate::{
9 Config, DnsProvider, RecordType,
10 digitalocean::types::{CreateUpdate, Record, Records},
11 errors::{Error, Result},
12 generate_helpers,
13 http::{self, ResponseToOption, WithHeaders},
14};
15
16const API_BASE: &str = "https://api.digitalocean.com/v2/domains";
17
18#[derive(Clone, Debug, Deserialize)]
22pub struct Auth {
23 pub key: String,
24}
25
26impl Auth {
27 fn get_header(&self) -> String {
28 format!("Bearer {}", self.key)
29 }
30}
31
32pub struct DigitalOcean {
36 config: Config,
37 auth: Auth,
38}
39
40impl DigitalOcean {
41 pub fn new(config: Config, auth: Auth) -> Self {
43 Self {
44 config,
45 auth,
46 }
47 }
48
49 fn get_upstream_records<T>(&self, rtype: &RecordType, host: &str) -> Result<Vec<Record<T>>>
50 where
51 T: DeserializeOwned
52 {
53 let url = format!("{API_BASE}/{}/records?type={rtype}&name={host}.{}", self.config.domain, self.config.domain);
54
55 let response = http::client().get(url)
56 .with_json_headers()
57 .with_auth(self.auth.get_header())
58 .call()?
59 .to_option()?;
60
61 let recs: Records<T> = match response {
63 Some(rec) => rec,
64 None => return Ok(vec![])
65 };
66
67 Ok(recs.domain_records)
68 }
69
70 fn get_upstream_record<T>(&self, rtype: &RecordType, host: &str) -> Result<Option<Record<T>>>
71 where
72 T: DeserializeOwned
73 {
74 let mut recs = self.get_upstream_records(rtype, host)?;
75
76 let nr = recs.len();
80 if nr > 1 {
81 error!("Returned number of records is {}, should be 1", nr);
82 return Err(Error::UnexpectedRecord(format!("Returned number of records is {nr}, should be 1")));
83 } else if nr == 0 {
84 warn!("No IP returned for {host}, continuing");
85 return Ok(None);
86 }
87
88 Ok(Some(recs.remove(0)))
89 }
90
91 fn do_delete(&self, rec: Record<String>) -> Result<()> {
92
93 let url = format!("{API_BASE}/{}/records/{}", self.config.domain, rec.id);
94 if self.config.dry_run {
95 info!("DRY-RUN: Would have sent DELETE to {url}");
96 return Ok(())
97 }
98
99 info!("Deleting DNS {} record {}", rec.rtype, rec.name);
100 http::client().delete(url)
101 .with_auth(self.auth.get_header())
102 .with_json_headers()
103 .call()?;
104
105 Ok(())
106 }
107
108}
109
110impl DnsProvider for DigitalOcean {
111
112 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T> >
113 where
114 T: DeserializeOwned
115 {
116 let rec: Record<T> = match self.get_upstream_record(&rtype, host)? {
117 Some(rec) => rec,
118 None => return Ok(None)
119 };
120
121 Ok(Some(rec.data))
122 }
123
124 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
125 where
126 T: Serialize + DeserializeOwned + Display + Clone
127 {
128 let url = format!("{API_BASE}/{}/records", self.config.domain);
129
130 let record = CreateUpdate {
131 name: host.to_string(),
132 rtype,
133 data: record.to_string(),
134 ttl: 300,
135 };
136 if self.config.dry_run {
137 info!("DRY-RUN: Would have sent {record:?} to {url}");
138 return Ok(())
139 }
140
141 let body = serde_json::to_string(&record)?;
142 let _response = http::client().post(url)
143 .with_auth(self.auth.get_header())
144 .with_json_headers()
145 .send(body)?
146 .check_error()?;
147
148 Ok(())
149 }
150
151 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
152 where
153 T: Serialize + DeserializeOwned + Display + Clone
154 {
155 let rec: Record<T> = self.get_upstream_record(&rtype, host)?
156 .ok_or(Error::RecordNotFound(host.to_string()))?;
157 let url = format!("{API_BASE}/{}/records/{}", self.config.domain, rec.id);
158
159 let record = CreateUpdate {
160 name: host.to_string(),
161 rtype,
162 data: urec.to_string(),
163 ttl: 300,
164 };
165
166 if self.config.dry_run {
167 info!("DRY-RUN: Would have sent {record:?} to {url}");
168 return Ok(())
169 }
170
171 let body = serde_json::to_string(&record)?;
172 let _response = http::client().put(url)
173 .with_auth(self.auth.get_header())
174 .with_json_headers()
175 .send(body)?
176 .check_error()?;
177
178 Ok(())
179 }
180
181 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
182 {
183 let rec = match self.get_upstream_record(&rtype, host)? {
184 Some(rec) => rec,
185 None => {
186 warn!("No {rtype} record to delete for {host}");
187 return Ok(());
188 }
189 };
190
191 self.do_delete(rec)
192 }
193
194 fn delete_all_records(&self, rtype: RecordType, host: &str) -> Result<()>
195 where Self: Sized
196 {
197 let recs: Vec<Record<String>> = self.get_upstream_records(&rtype, host)?;
198 for rec in recs {
199 self.do_delete(rec)?;
200 }
201
202 Ok(())
203 }
204
205 generate_helpers!();
206
207}
208
209
210#[cfg(test)]
211pub(crate) mod tests {
212 use super::*;
213 use crate::{generate_tests, tests::*};
214 use std::env;
215
216 fn get_client() -> DigitalOcean {
217 let auth = Auth {
218 key: env::var("DIGITALOCEAN_API_KEY").unwrap(),
219 };
220 let config = Config {
221 domain: env::var("DIGITALOCEAN_TEST_DOMAIN").unwrap(),
222 dry_run: false,
223 };
224 DigitalOcean::new(config, auth)
225 }
226
227 generate_tests!("test_digitalocean");
228}