Skip to main content

dnslib/vendors/cloudflare/
mod.rs

1pub 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        // SAFETY: callers hold ENV_LOCK while mutating these process-wide env vars.
47        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        // SAFETY: this test serializes access to these process-wide env vars.
124        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        // SAFETY: this test serializes access to these process-wide env vars.
144        clear_cloudflare_env();
145    }
146}