1#![allow(unused)]
2
3mod types;
4
5use std::{fmt::Display};
6use serde::{de::DeserializeOwned, Serialize};
7use tracing::{error, info, warn};
8
9use types::{Record, RecordUpdate};
10use crate::{errors::{Error, Result}, http, Config, DnsProvider, RecordType};
11
12const API_BASE: &str = "https://api.gandi.net/v5/livedns";
13
14pub enum Auth {
15 ApiKey(String),
16 PatKey(String),
17}
18
19impl Auth {
20 fn get_header(&self) -> String {
21 match self {
22 Auth::ApiKey(key) => format!("Apikey {key}"),
23 Auth::PatKey(key) => format!("Bearer {key}"),
24 }
25 }
26}
27
28
29pub struct Gandi {
30 pub config: Config,
31 pub auth: Auth,
32}
33
34impl Gandi {
35 pub fn new(config: Config, auth: Auth) -> Self {
36 Gandi {
37 config,
38 auth,
39 }
40 }
41}
42
43impl DnsProvider for Gandi {
44
45 async fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
46 where
47 T: DeserializeOwned
48 {
49 let url = format!("{API_BASE}/domains/{}/records/{host}/{rtype}", self.config.domain)
50 .parse()
51 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
52 let auth = self.auth.get_header();
53 let mut rec: Record<T> = match http::get(url, Some(auth)).await? {
54 Some(rec) => rec,
55 None => return Ok(None)
56 };
57
58 let nr = rec.rrset_values.len();
59
60 if nr > 1 {
63 error!("Returned number of IPs is {}, should be 1", nr);
64 return Err(Error::UnexpectedRecord(format!("Returned number of IPs is {nr}, should be 1")));
65 } else if nr == 0 {
66 warn!("No IP returned for {host}, continuing");
67 return Ok(None);
68 }
69
70 Ok(Some(rec.rrset_values.remove(0)))
71
72 }
73
74 async fn create_record<T>(&self, rtype: RecordType, host: &str, rec: &T) -> Result<()>
75 where
76 T: Serialize + DeserializeOwned + Display + Clone + Send + Sync
77 {
78 self.update_record(rtype, host, rec).await
80 }
81
82 async fn update_record<T>(&self, rtype: RecordType, host: &str, ip: &T) -> Result<()>
83 where
84 T: Serialize + DeserializeOwned + Display + Clone + Send + Sync
85 {
86 let url = format!("{API_BASE}/domains/{}/records/{host}/{rtype}", self.config.domain)
87 .parse()
88 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
89 let auth = self.auth.get_header();
90
91 let update = RecordUpdate {
92 rrset_values: vec![(*ip).clone()],
93 rrset_ttl: Some(300),
94 };
95 if self.config.dry_run {
96 info!("DRY-RUN: Would have sent PUT to {url}");
97 return Ok(())
98 }
99 http::put::<RecordUpdate<T>>(url, &update, Some(auth)).await?;
100 Ok(())
101 }
102
103 async fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()> {
104 let url = format!("{API_BASE}/domains/{}/records/{host}/{rtype}", self.config.domain)
105 .parse()
106 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
107 let auth = self.auth.get_header();
108
109 if self.config.dry_run {
110 info!("DRY-RUN: Would have sent DELETE to {url}");
111 return Ok(())
112 }
113 http::delete(url, Some(auth)).await?;
114
115 Ok(())
116 }
117
118}
119
120#[cfg(test)]
121mod tests {
122 use crate::strip_quotes;
123
124 use super::*;
125 use std::{env, net::Ipv4Addr};
126 use macro_rules_attribute::apply;
127 use random_string::charsets::ALPHANUMERIC;
128 use tracing_test::traced_test;
129
130 fn get_client() -> Gandi {
131 let auth = if let Some(key) = env::var("GANDI_APIKEY").ok() {
132 Auth::ApiKey(key)
133 } else if let Some(key) = env::var("GANDI_PATKEY").ok() {
134 Auth::PatKey(key)
135 } else {
136 panic!("No Gandi auth key set");
137 };
138
139 let config = Config {
140 domain: env::var("GANDI_TEST_DOMAIN").unwrap(),
141 dry_run: false,
142 };
143
144 Gandi {
145 config,
146 auth,
147 }
148 }
149
150
151 async fn test_create_update_delete_ipv4() -> Result<()> {
153 let client = get_client();
154
155 let host = random_string::generate(16, ALPHANUMERIC);
156
157 let ip: Ipv4Addr = "1.1.1.1".parse()?;
159 client.create_record(RecordType::A, &host, &ip).await?;
160 let cur = client.get_record(RecordType::A, &host).await?;
161 assert_eq!(Some(ip), cur);
162
163
164 let ip: Ipv4Addr = "2.2.2.2".parse()?;
166 client.update_record(RecordType::A, &host, &ip).await?;
167 let cur = client.get_record(RecordType::A, &host).await?;
168 assert_eq!(Some(ip), cur);
169
170
171 client.delete_record(RecordType::A, &host).await?;
173 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host).await?;
174 assert!(del.is_none());
175
176 Ok(())
177 }
178
179 async fn test_create_update_delete_txt() -> Result<()> {
180 let client = get_client();
181
182 let host = random_string::generate(16, ALPHANUMERIC);
183
184 let txt = "a text reference".to_string();
186 client.create_record(RecordType::TXT, &host, &txt).await?;
187 let cur: Option<String> = client.get_record(RecordType::TXT, &host).await?;
188 assert_eq!(txt, strip_quotes(&cur.unwrap()));
189
190
191 let txt = "another text reference".to_string();
193 client.update_record(RecordType::TXT, &host, &txt).await?;
194 let cur: Option<String> = client.get_record(RecordType::TXT, &host).await?;
195 assert_eq!(txt, strip_quotes(&cur.unwrap()));
196
197
198 client.delete_record(RecordType::TXT, &host).await?;
200 let del: Option<String> = client.get_record(RecordType::TXT, &host).await?;
201 assert!(del.is_none());
202
203 Ok(())
204 }
205
206
207 #[cfg(feature = "smol")]
208 mod smol_tests {
209 use super::*;
210 use macro_rules_attribute::apply;
211 use smol_macros::test;
212
213 #[apply(test!)]
214 #[traced_test]
215 #[cfg_attr(not(feature = "test_gandi"), ignore = "Gandi API test")]
216 async fn smol_create_update_a() -> Result<()> {
217 test_create_update_delete_ipv4().await?;
218 Ok(())
219 }
220
221 #[apply(test!)]
222 #[traced_test]
223 #[cfg_attr(not(feature = "test_gandi"), ignore = "Gandi API test")]
224 async fn smol_create_update_txt() -> Result<()> {
225 test_create_update_delete_ipv4().await?;
226 Ok(())
227 }
228 }
229
230 #[cfg(feature = "tokio")]
231 mod tokio_tests {
232 use super::*;
233
234 #[tokio::test]
235 #[traced_test]
236 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
237 async fn tokio_create_update() -> Result<()> {
238 test_create_update_delete_ipv4().await?;
239 test_create_update_delete_txt().await?;
240 Ok(())
241 }
242 }
243
244}