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: &'static 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_record<T>(&self, rtype: &RecordType, host: &str) -> Result<Option<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 mut recs: Records<T> = match response {
63 Some(rec) => rec,
64 None => return Ok(None)
65 };
66
67 let nr = recs.domain_records.len();
71 if nr > 1 {
72 error!("Returned number of records is {}, should be 1", nr);
73 return Err(Error::UnexpectedRecord(format!("Returned number of records is {nr}, should be 1")));
74 } else if nr == 0 {
75 warn!("No IP returned for {host}, continuing");
76 return Ok(None);
77 }
78
79 Ok(Some(recs.domain_records.remove(0)))
80 }
81
82 fn get_record_id(&self, rtype: &RecordType, host: &str) -> Result<Option<u64>> {
83 let id_p = self.get_upstream_record::<String>(rtype, host)?
84 .map(|r| r.id);
85 Ok(id_p)
86 }
87}
88
89impl DnsProvider for DigitalOcean {
90
91 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T> >
92 where
93 T: DeserializeOwned
94 {
95 let rec: Record<T> = match self.get_upstream_record(&rtype, host)? {
96 Some(rec) => rec,
97 None => return Ok(None)
98 };
99
100 Ok(Some(rec.data))
101 }
102
103 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
104 where
105 T: Serialize + DeserializeOwned + Display + Clone
106 {
107 let url = format!("{API_BASE}/{}/records", self.config.domain);
108
109 let record = CreateUpdate {
110 name: host.to_string(),
111 rtype,
112 data: record.to_string(),
113 ttl: 300,
114 };
115 if self.config.dry_run {
116 info!("DRY-RUN: Would have sent {record:?} to {url}");
117 return Ok(())
118 }
119
120 let body = serde_json::to_string(&record)?;
121 let _response = http::client().post(url)
122 .with_auth(self.auth.get_header())
123 .with_json_headers()
124 .send(body)?
125 .check_error()?;
126
127 Ok(())
128 }
129
130 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
131 where
132 T: Serialize + DeserializeOwned + Display + Clone
133 {
134 let id = self.get_record_id(&rtype, host)?
135 .ok_or(Error::RecordNotFound(host.to_string()))?;
136 let url = format!("{API_BASE}/{}/records/{id}", self.config.domain);
137
138 let record = CreateUpdate {
139 name: host.to_string(),
140 rtype,
141 data: urec.to_string(),
142 ttl: 300,
143 };
144
145 if self.config.dry_run {
146 info!("DRY-RUN: Would have sent {record:?} to {url}");
147 return Ok(())
148 }
149
150 let body = serde_json::to_string(&record)?;
151 let _response = http::client().put(url)
152 .with_auth(self.auth.get_header())
153 .with_json_headers()
154 .send(body)?
155 .check_error()?;
156
157 Ok(())
158 }
159
160 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
161 {
162 let id = match self.get_record_id(&rtype, host)? {
163 Some(id) => id,
164 None => {
165 warn!("No {rtype} record to delete for {host}");
166 return Ok(());
167 }
168 };
169
170 let url = format!("{API_BASE}/{}/records/{id}", self.config.domain);
171 if self.config.dry_run {
172 info!("DRY-RUN: Would have sent DELETE to {url}");
173 return Ok(())
174 }
175
176 http::client().delete(url)
177 .with_auth(self.auth.get_header())
178 .with_json_headers()
179 .call()?;
180
181 Ok(())
182 }
183
184 generate_helpers!();
185
186}
187
188
189#[cfg(test)]
190pub(crate) mod tests {
191 use super::*;
192 use crate::{generate_tests, tests::*};
193 use std::env;
194
195 fn get_client() -> DigitalOcean {
196 let auth = Auth {
197 key: env::var("DIGITALOCEAN_API_KEY").unwrap(),
198 };
199 let config = Config {
200 domain: env::var("DIGITALOCEAN_TEST_DOMAIN").unwrap(),
201 dry_run: false,
202 };
203 DigitalOcean::new(config, auth)
204 }
205
206 generate_tests!("test_digitalocean");
207}