1#![allow(unused)]
2
3mod types;
4
5use std::net::Ipv4Addr;
6use tracing::{error, info, warn};
7
8use types::{Record, RecordUpdate};
9use crate::{errors::{Error, Result}, http, Config, DnsProvider};
10
11const API_BASE: &str = "https://api.gandi.net/v5/livedns";
12
13pub enum Auth {
14 ApiKey(String),
15 PatKey(String),
16}
17
18impl Auth {
19 fn get_header(&self) -> String {
20 match self {
21 Auth::ApiKey(key) => format!("Apikey {key}"),
22 Auth::PatKey(key) => format!("Bearer {key}"),
23 }
24 }
25}
26
27
28pub struct Gandi {
29 pub config: Config,
30 pub auth: Auth,
31}
32
33impl Gandi {
34 pub fn new(config: Config, auth: Auth) -> Self {
35 Gandi {
36 config,
37 auth,
38 }
39 }
40}
41
42impl DnsProvider for Gandi {
43
44 async fn get_v4_record(&self, host: &str) -> Result<Option<Ipv4Addr>> {
45 let url = format!("{API_BASE}/domains/{}/records/{host}/A", self.config.domain)
46 .parse()
47 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
48 let auth = self.auth.get_header();
49 let rec: Record = match http::get(url, Some(auth)).await? {
50 Some(rec) => rec,
51 None => return Ok(None)
52 };
53
54 let nr = rec.rrset_values.len();
55
56 if nr > 1 {
59 error!("Returned number of IPs is {}, should be 1", nr);
60 return Err(Error::UnexpectedRecord(format!("Returned number of IPs is {nr}, should be 1")));
61 } else if nr == 0 {
62 warn!("No IP returned for {host}, continuing");
63 return Ok(None);
64 }
65
66 Ok(Some(rec.rrset_values[0]))
67
68 }
69
70 async fn create_v4_record(&self, host: &str,ip: &Ipv4Addr) -> Result<()> {
71 self.update_v4_record(host, ip).await
73 }
74
75 async fn update_v4_record(&self, host: &str, ip: &Ipv4Addr) -> Result<()> {
76 let url = format!("{API_BASE}/domains/{}/records/{host}/A", self.config.domain)
77 .parse()
78 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
79 let auth = self.auth.get_header();
80
81 let update = RecordUpdate {
82 rrset_values: vec![*ip],
83 rrset_ttl: Some(300),
84 };
85 if self.config.dry_run {
86 info!("DRY-RUN: Would have sent {update:?} to {url}");
87 return Ok(())
88 }
89 http::put::<RecordUpdate>(url, &update, Some(auth)).await?;
90 Ok(())
91 }
92
93 async fn delete_v4_record(&self,host: &str) -> Result<()> {
94 let url = format!("{API_BASE}/domains/{}/records/{host}/A", self.config.domain)
95 .parse()
96 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
97 let auth = self.auth.get_header();
98
99 if self.config.dry_run {
100 info!("DRY-RUN: Would have sent DELETE to {url}");
101 return Ok(())
102 }
103 http::delete(url, Some(auth)).await?;
104
105 Ok(())
106 }
107
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::env;
114 use macro_rules_attribute::apply;
115 use random_string::charsets::ALPHANUMERIC;
116 use smol_macros::test;
117 use tracing_test::traced_test;
118
119 fn get_client() -> Gandi {
120 let auth = if let Some(key) = env::var("GANDI_APIKEY").ok() {
121 Auth::ApiKey(key)
122 } else if let Some(key) = env::var("GANDI_PATKEY").ok() {
123 Auth::PatKey(key)
124 } else {
125 panic!("No Gandi auth key set");
126 };
127
128 let config = Config {
129 domain: env::var("GANDI_TEST_DOMAIN").unwrap(),
130 dry_run: false,
131 };
132
133 Gandi {
134 config,
135 auth,
136 }
137 }
138
139
140 async fn test_create_update_delete_ipv4() -> Result<()> {
142 let client = get_client();
143
144 let host = random_string::generate(16, ALPHANUMERIC);
145
146 let ip = "1.1.1.1".parse()?;
148 client.create_v4_record(&host, &ip).await?;
149 let cur = client.get_v4_record(&host).await?;
150 assert_eq!(Some(ip), cur);
151
152
153 let ip = "2.2.2.2".parse()?;
155 client.update_v4_record(&host, &ip).await?;
156 let cur = client.get_v4_record(&host).await?;
157 assert_eq!(Some(ip), cur);
158
159
160 client.delete_v4_record(&host).await?;
162 let del = client.get_v4_record(&host).await?;
163 assert!(del.is_none());
164
165 Ok(())
166 }
167
168
169 #[cfg(feature = "smol")]
170 mod smol {
171 use super::*;
172 use macro_rules_attribute::apply;
173 use smol_macros::test;
174
175 #[apply(test!)]
176 #[traced_test]
177 #[cfg_attr(not(feature = "test_gandi"), ignore = "Gandi API test")]
178 async fn smol_create_update() -> Result<()> {
179 test_create_update_delete_ipv4().await
180 }
181 }
182
183 #[cfg(feature = "tokio")]
184 mod smol {
185 use super::*;
186
187 #[tokio::test]
188 #[traced_test]
189 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
190 async fn tokio_create_update() -> Result<()> {
191 test_create_update_delete_ipv4().await
192 }
193 }
194
195}