dnslib/vendors/cloudflare/
mod.rs1pub mod client;
2pub mod mapping;
3pub mod service;
4
5use std::env;
6
7use crate::control_plane::config::{self as app_config, DnsServerConfig};
8use crate::core::error::{Error, Result};
9use crate::core::secret::ApiToken;
10use crate::vendors::runtime::ClientOverrides;
11
12pub fn client_from_server(
13 server: &DnsServerConfig,
14 overrides: ClientOverrides<'_>,
15) -> Result<client::CloudflareClient> {
16 let base_url = overrides
17 .base_url
18 .map(ToOwned::to_owned)
19 .or_else(|| env::var("DNSYNC_CLOUDFLARE_BASE_URL").ok())
20 .or_else(|| server.base_url_env.as_ref().and_then(|k| env::var(k).ok()))
21 .or_else(|| server.base_url.clone())
22 .unwrap_or_else(|| app_config::CLOUDFLARE_DEFAULT_BASE_URL.to_string());
23 let token = overrides
24 .token
25 .map(ToOwned::to_owned)
26 .or_else(|| env::var("DNSYNC_CLOUDFLARE_API_TOKEN").ok())
27 .or_else(|| server.token_env.as_ref().and_then(|k| env::var(k).ok()))
28 .or_else(|| server.token.clone())
29 .ok_or_else(|| {
30 Error::parse(
31 "Cloudflare API token is required from --token, DNSYNC_CLOUDFLARE_API_TOKEN, token_env, or config token",
32 )
33 })
34 .map(ApiToken::new)?;
35 client::CloudflareClient::new(base_url, token)
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41 use crate::core::dns::service::DnsVendor;
42
43 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
44
45 fn clear_cloudflare_env() {
46 unsafe {
48 std::env::remove_var("DNSYNC_CLOUDFLARE_BASE_URL");
49 std::env::remove_var("DNSYNC_CLOUDFLARE_API_TOKEN");
50 }
51 }
52
53 #[test]
54 fn client_uses_config_token() {
55 let _guard = ENV_LOCK.lock().unwrap();
56 clear_cloudflare_env();
57 let app_config: app_config::AppConfig = toml::from_str(
58 r#"
59 [[servers]]
60 id = "cf"
61 vendor = "cloudflare"
62 token = "config-token"
63 "#,
64 )
65 .unwrap();
66 let server = app_config.selected_server(Some("cf")).unwrap();
67
68 let client = client_from_server(server, ClientOverrides::default()).unwrap();
69
70 assert_eq!(client.kind(), app_config::VendorKind::Cloudflare);
71 }
72
73 #[test]
74 fn cli_token_wins_over_config() {
75 let _guard = ENV_LOCK.lock().unwrap();
76 clear_cloudflare_env();
77 let app_config: app_config::AppConfig = toml::from_str(
78 r#"
79 [[servers]]
80 id = "cf"
81 vendor = "cloudflare"
82 token = "config-token"
83 "#,
84 )
85 .unwrap();
86 let server = app_config.selected_server(Some("cf")).unwrap();
87
88 let client = client_from_server(
89 server,
90 ClientOverrides {
91 token: Some("cli-token"),
92 ..ClientOverrides::default()
93 },
94 )
95 .unwrap();
96
97 assert_eq!(client.kind(), app_config::VendorKind::Cloudflare);
98 }
99
100 #[test]
101 fn errors_without_token() {
102 let _guard = ENV_LOCK.lock().unwrap();
103 clear_cloudflare_env();
104 let app_config: app_config::AppConfig = toml::from_str(
105 r#"
106 [[servers]]
107 id = "cf"
108 vendor = "cloudflare"
109 "#,
110 )
111 .unwrap();
112 let server = app_config.selected_server(Some("cf")).unwrap();
113
114 let err = client_from_server(server, ClientOverrides::default()).unwrap_err();
115
116 assert!(err.to_string().contains("Cloudflare API token"));
117 }
118
119 #[test]
120 fn client_from_server_uses_vendor_env_without_overrides() {
121 let _guard = ENV_LOCK.lock().unwrap();
122 clear_cloudflare_env();
123 unsafe {
125 std::env::set_var("DNSYNC_CLOUDFLARE_BASE_URL", "https://cf.example/client/v4");
126 std::env::set_var("DNSYNC_CLOUDFLARE_API_TOKEN", "cf-env-token");
127 }
128 let app_config: app_config::AppConfig = toml::from_str(
129 r#"
130 [[servers]]
131 id = "cf"
132 vendor = "cloudflare"
133 "#,
134 )
135 .unwrap();
136 let server = app_config.selected_server(Some("cf")).unwrap();
137
138 let client = client_from_server(server, ClientOverrides::default()).unwrap();
139
140 assert_eq!(client.base_url(), "https://cf.example/client/v4");
141 assert_eq!(client.kind(), app_config::VendorKind::Cloudflare);
142
143 clear_cloudflare_env();
145 }
146}