1use std::{
13 net::{Ipv4Addr, Ipv6Addr},
14 time::Duration,
15};
16
17use crate::{http::HttpClientBuilder, strip_origin_from_name, DnsRecord, Error, IntoFqdn};
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone)]
21pub struct DigitalOceanProvider {
22 client: HttpClientBuilder,
23}
24
25#[derive(Deserialize, Serialize, Clone, Debug)]
26pub struct ListDomainRecord {
27 domain_records: Vec<DomainRecord>,
28}
29
30#[derive(Deserialize, Serialize, Clone, Debug)]
31pub struct UpdateDomainRecord<'a> {
32 ttl: u32,
33 name: &'a str,
34 #[serde(flatten)]
35 data: RecordData,
36}
37
38#[derive(Deserialize, Serialize, Clone, Debug)]
39pub struct DomainRecord {
40 id: i64,
41 ttl: u32,
42 name: String,
43 #[serde(flatten)]
44 data: RecordData,
45}
46
47#[derive(Deserialize, Serialize, Clone, Debug)]
48#[serde(tag = "type")]
49#[allow(clippy::upper_case_acronyms)]
50pub enum RecordData {
51 A {
52 data: Ipv4Addr,
53 },
54 AAAA {
55 data: Ipv6Addr,
56 },
57 CNAME {
58 data: String,
59 },
60 NS {
61 data: String,
62 },
63 MX {
64 data: String,
65 priority: u16,
66 },
67 TXT {
68 data: String,
69 },
70 SRV {
71 data: String,
72 priority: u16,
73 port: u16,
74 weight: u16,
75 },
76}
77
78#[derive(Serialize, Debug)]
79pub struct Query<'a> {
80 name: &'a str,
81}
82
83impl DigitalOceanProvider {
84 pub(crate) fn new(auth_token: impl AsRef<str>, timeout: Option<Duration>) -> Self {
85 let client = HttpClientBuilder::default()
86 .with_header("Authorization", format!("Bearer {}", auth_token.as_ref()))
87 .with_timeout(timeout);
88 Self { client }
89 }
90
91 pub(crate) async fn create(
92 &self,
93 name: impl IntoFqdn<'_>,
94 record: DnsRecord,
95 ttl: u32,
96 origin: impl IntoFqdn<'_>,
97 ) -> crate::Result<()> {
98 let name = name.into_name();
99 let domain = origin.into_name();
100 let subdomain = strip_origin_from_name(&name, &domain);
101
102 self.client
103 .post(format!(
104 "https://api.digitalocean.com/v2/domains/{domain}/records",
105 ))
106 .with_body(UpdateDomainRecord {
107 ttl,
108 name: &subdomain,
109 data: record.into(),
110 })?
111 .send_raw()
112 .await
113 .map(|_| ())
114 }
115
116 pub(crate) async fn update(
117 &self,
118 name: impl IntoFqdn<'_>,
119 record: DnsRecord,
120 ttl: u32,
121 origin: impl IntoFqdn<'_>,
122 ) -> crate::Result<()> {
123 let name = name.into_name();
124 let domain = origin.into_name();
125 let subdomain = strip_origin_from_name(&name, &domain);
126 let record_id = self.obtain_record_id(&name, &domain).await?;
127
128 self.client
129 .put(format!(
130 "https://api.digitalocean.com/v2/domains/{domain}/records/{record_id}",
131 ))
132 .with_body(UpdateDomainRecord {
133 ttl,
134 name: &subdomain,
135 data: record.into(),
136 })?
137 .send_raw()
138 .await
139 .map(|_| ())
140 }
141
142 pub(crate) async fn delete(
143 &self,
144 name: impl IntoFqdn<'_>,
145 origin: impl IntoFqdn<'_>,
146 ) -> crate::Result<()> {
147 let name = name.into_name();
148 let domain = origin.into_name();
149 let record_id = self.obtain_record_id(&name, &domain).await?;
150
151 self.client
152 .delete(format!(
153 "https://api.digitalocean.com/v2/domains/{domain}/records/{record_id}",
154 ))
155 .send_raw()
156 .await
157 .map(|_| ())
158 }
159
160 async fn obtain_record_id(&self, name: &str, domain: &str) -> crate::Result<i64> {
161 let subdomain = strip_origin_from_name(name, domain);
162 self.client
163 .get(format!(
164 "https://api.digitalocean.com/v2/domains/{domain}/records?{}",
165 Query::name(name).serialize()
166 ))
167 .send_with_retry::<ListDomainRecord>(3)
168 .await
169 .and_then(|result| {
170 result
171 .domain_records
172 .into_iter()
173 .find(|record| record.name == subdomain)
174 .map(|record| record.id)
175 .ok_or_else(|| Error::Api(format!("DNS Record {} not found", subdomain)))
176 })
177 }
178}
179
180impl<'a> Query<'a> {
181 pub fn name(name: impl Into<&'a str>) -> Self {
182 Self { name: name.into() }
183 }
184
185 pub fn serialize(&self) -> String {
186 serde_urlencoded::to_string(self).unwrap()
187 }
188}
189
190impl From<DnsRecord> for RecordData {
191 fn from(record: DnsRecord) -> Self {
192 match record {
193 DnsRecord::A { content } => RecordData::A { data: content },
194 DnsRecord::AAAA { content } => RecordData::AAAA { data: content },
195 DnsRecord::CNAME { content } => RecordData::CNAME { data: content },
196 DnsRecord::NS { content } => RecordData::NS { data: content },
197 DnsRecord::MX { content, priority } => RecordData::MX {
198 data: content,
199 priority,
200 },
201 DnsRecord::TXT { content } => RecordData::TXT { data: content },
202 DnsRecord::SRV {
203 content,
204 priority,
205 weight,
206 port,
207 } => RecordData::SRV {
208 data: content,
209 priority,
210 weight,
211 port,
212 },
213 }
214 }
215}