Skip to main content

xbp_cli/provider_support/
cloudflare.rs

1use super::http::{AuthStrategy, HttpError, RequestFactory, ResponseBytes};
2use super::models::{
3    CloudflareDnsRecord, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
4    CloudflareDnssec, CloudflareDnssecEdit, CloudflareEnvelope, CloudflareZone,
5    CloudflareZoneFilters, DomainAvailability, PaginationInfo, RegisteredDomain, SecretRecord,
6    SecretsQuota, SecretsStore,
7};
8use reqwest::header::{HeaderName, HeaderValue};
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11
12const CLOUDFLARE_API_BASE: &str = "https://api.cloudflare.com/client/v4";
13
14#[derive(Debug, Clone)]
15pub struct CloudflareClient {
16    account_id: String,
17    http: RequestFactory,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CloudflareStoreCreateRequest {
22    pub name: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CloudflareSecretCreateRequest {
27    pub name: String,
28    pub value: String,
29    #[serde(default)]
30    pub scopes: Vec<String>,
31    #[serde(default)]
32    pub comment: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct CloudflareSecretEditRequest {
37    #[serde(default)]
38    pub name: Option<String>,
39    #[serde(default)]
40    pub value: Option<String>,
41    #[serde(default)]
42    pub scopes: Option<Vec<String>>,
43    #[serde(default)]
44    pub comment: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CloudflareSecretDuplicateRequest {
49    pub name: String,
50    #[serde(default)]
51    pub scopes: Vec<String>,
52    #[serde(default)]
53    pub comment: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct CloudflareBulkDeleteRequest {
58    pub ids: Vec<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Default)]
62pub struct CloudflareZoneCreateRequest {
63    pub name: String,
64    #[serde(default)]
65    pub account: Option<CloudflareZoneAccountWrite>,
66    #[serde(default)]
67    pub jump_start: Option<bool>,
68    #[serde(rename = "type", default)]
69    pub zone_type: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Default)]
73pub struct CloudflareZoneEditRequest {
74    #[serde(default)]
75    pub paused: Option<bool>,
76    #[serde(rename = "type", default)]
77    pub zone_type: Option<String>,
78    #[serde(default)]
79    pub vanity_name_servers: Option<Vec<String>>,
80}
81
82#[derive(Debug, Clone, Serialize, Default)]
83pub struct CloudflareZoneAccountWrite {
84    #[serde(default)]
85    pub id: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Default)]
89pub struct CloudflareDnsRecordListFilters {
90    #[serde(default)]
91    pub name: Option<String>,
92    #[serde(rename = "type", default)]
93    pub record_type: Option<String>,
94    #[serde(default)]
95    pub page: Option<u64>,
96    #[serde(default)]
97    pub per_page: Option<u64>,
98}
99
100impl CloudflareClient {
101    pub fn new(
102        api_token: impl Into<String>,
103        account_id: impl Into<String>,
104    ) -> Result<Self, String> {
105        let token = api_token.into();
106        let http = RequestFactory::new(CLOUDFLARE_API_BASE)
107            .map_err(|error| error.to_string())?
108            .with_auth(AuthStrategy::Bearer(token))
109            .with_default_header(
110                HeaderName::from_static("content-type"),
111                HeaderValue::from_static("application/json"),
112            );
113        Ok(Self {
114            account_id: account_id.into(),
115            http,
116        })
117    }
118
119    pub async fn search_domains(
120        &self,
121        query: &str,
122        extension: &[String],
123    ) -> Result<Vec<DomainAvailability>, String> {
124        #[derive(Serialize)]
125        struct SearchQuery<'a> {
126            query: &'a str,
127            #[serde(skip_serializing_if = "<[String]>::is_empty")]
128            tlds: &'a [String],
129        }
130
131        let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
132            .http
133            .get_json(
134                &format!("/accounts/{}/registrar/domain-search", self.account_id),
135                Some(&SearchQuery {
136                    query,
137                    tlds: extension,
138                }),
139            )
140            .await
141            .map_err(render_http_error)?;
142        Ok(envelope.result)
143    }
144
145    pub async fn check_domains(
146        &self,
147        domains: &[String],
148    ) -> Result<Vec<DomainAvailability>, String> {
149        let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
150            .http
151            .post_json(
152                &format!("/accounts/{}/registrar/domain-check", self.account_id),
153                &json!({ "domains": domains }),
154            )
155            .await
156            .map_err(render_http_error)?;
157        Ok(envelope.result)
158    }
159
160    pub async fn list_registered_domains(
161        &self,
162    ) -> Result<(Vec<RegisteredDomain>, Option<PaginationInfo>), String> {
163        let envelope: CloudflareEnvelope<Vec<RegisteredDomain>> = self
164            .http
165            .get_json(
166                &format!("/accounts/{}/registrar/domains", self.account_id),
167                Option::<&Value>::None,
168            )
169            .await
170            .map_err(render_http_error)?;
171        Ok((envelope.result, envelope.result_info))
172    }
173
174    pub async fn list_stores(&self) -> Result<(Vec<SecretsStore>, Option<PaginationInfo>), String> {
175        let envelope: CloudflareEnvelope<Vec<SecretsStore>> = self
176            .http
177            .get_json(
178                &format!("/accounts/{}/secrets_store/stores", self.account_id),
179                Option::<&Value>::None,
180            )
181            .await
182            .map_err(render_http_error)?;
183        Ok((envelope.result, envelope.result_info))
184    }
185
186    pub async fn get_store(&self, store_id: &str) -> Result<SecretsStore, String> {
187        let envelope: CloudflareEnvelope<SecretsStore> = self
188            .http
189            .get_json(
190                &format!(
191                    "/accounts/{}/secrets_store/stores/{}",
192                    self.account_id, store_id
193                ),
194                Option::<&Value>::None,
195            )
196            .await
197            .map_err(render_http_error)?;
198        Ok(envelope.result)
199    }
200
201    pub async fn create_store(
202        &self,
203        request: &CloudflareStoreCreateRequest,
204    ) -> Result<SecretsStore, String> {
205        let envelope: CloudflareEnvelope<SecretsStore> = self
206            .http
207            .post_json(
208                &format!("/accounts/{}/secrets_store/stores", self.account_id),
209                request,
210            )
211            .await
212            .map_err(render_http_error)?;
213        Ok(envelope.result)
214    }
215
216    pub async fn delete_store(&self, store_id: &str) -> Result<(), String> {
217        let _: CloudflareEnvelope<Option<Value>> = self
218            .http
219            .delete_json(
220                &format!(
221                    "/accounts/{}/secrets_store/stores/{}",
222                    self.account_id, store_id
223                ),
224                Option::<&Value>::None,
225            )
226            .await
227            .map_err(render_http_error)?;
228        Ok(())
229    }
230
231    pub async fn list_secrets(
232        &self,
233        store_id: &str,
234    ) -> Result<(Vec<SecretRecord>, Option<PaginationInfo>), String> {
235        let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
236            .http
237            .get_json(
238                &format!(
239                    "/accounts/{}/secrets_store/stores/{}/secrets",
240                    self.account_id, store_id
241                ),
242                Option::<&Value>::None,
243            )
244            .await
245            .map_err(render_http_error)?;
246        Ok((envelope.result, envelope.result_info))
247    }
248
249    pub async fn get_secret(
250        &self,
251        store_id: &str,
252        secret_id: &str,
253    ) -> Result<SecretRecord, String> {
254        let envelope: CloudflareEnvelope<SecretRecord> = self
255            .http
256            .get_json(
257                &format!(
258                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
259                    self.account_id, store_id, secret_id
260                ),
261                Option::<&Value>::None,
262            )
263            .await
264            .map_err(render_http_error)?;
265        Ok(envelope.result)
266    }
267
268    pub async fn create_secret(
269        &self,
270        store_id: &str,
271        request: &CloudflareSecretCreateRequest,
272    ) -> Result<SecretRecord, String> {
273        let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
274            .http
275            .post_json(
276                &format!(
277                    "/accounts/{}/secrets_store/stores/{}/secrets",
278                    self.account_id, store_id
279                ),
280                &vec![request],
281            )
282            .await
283            .map_err(render_http_error)?;
284        envelope
285            .result
286            .into_iter()
287            .next()
288            .ok_or_else(|| "Cloudflare did not return a created secret.".to_string())
289    }
290
291    pub async fn edit_secret(
292        &self,
293        store_id: &str,
294        secret_id: &str,
295        request: &CloudflareSecretEditRequest,
296    ) -> Result<SecretRecord, String> {
297        let envelope: CloudflareEnvelope<SecretRecord> = self
298            .http
299            .patch_json(
300                &format!(
301                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
302                    self.account_id, store_id, secret_id
303                ),
304                request,
305            )
306            .await
307            .map_err(render_http_error)?;
308        Ok(envelope.result)
309    }
310
311    pub async fn delete_secret(&self, store_id: &str, secret_id: &str) -> Result<(), String> {
312        let _: CloudflareEnvelope<Option<Value>> = self
313            .http
314            .delete_json(
315                &format!(
316                    "/accounts/{}/secrets_store/stores/{}/secrets/{}",
317                    self.account_id, store_id, secret_id
318                ),
319                Option::<&Value>::None,
320            )
321            .await
322            .map_err(render_http_error)?;
323        Ok(())
324    }
325
326    pub async fn bulk_delete_secrets(
327        &self,
328        store_id: &str,
329        request: &CloudflareBulkDeleteRequest,
330    ) -> Result<(), String> {
331        let _: CloudflareEnvelope<Option<Value>> = self
332            .http
333            .delete_json_with_body(
334                &format!(
335                    "/accounts/{}/secrets_store/stores/{}/secrets",
336                    self.account_id, store_id
337                ),
338                Option::<&Value>::None,
339                request,
340            )
341            .await
342            .map_err(render_http_error)?;
343        Ok(())
344    }
345
346    pub async fn duplicate_secret(
347        &self,
348        store_id: &str,
349        secret_id: &str,
350        request: &CloudflareSecretDuplicateRequest,
351    ) -> Result<SecretRecord, String> {
352        let envelope: CloudflareEnvelope<SecretRecord> = self
353            .http
354            .post_json(
355                &format!(
356                    "/accounts/{}/secrets_store/stores/{}/secrets/{}/duplicate",
357                    self.account_id, store_id, secret_id
358                ),
359                request,
360            )
361            .await
362            .map_err(render_http_error)?;
363        Ok(envelope.result)
364    }
365
366    pub async fn get_quota(&self) -> Result<SecretsQuota, String> {
367        let envelope: CloudflareEnvelope<SecretsQuota> = self
368            .http
369            .get_json(
370                &format!("/accounts/{}/secrets_store/quota", self.account_id),
371                Option::<&Value>::None,
372            )
373            .await
374            .map_err(render_http_error)?;
375        Ok(envelope.result)
376    }
377
378    pub async fn list_zones(
379        &self,
380        filters: &CloudflareZoneFilters,
381    ) -> Result<(Vec<CloudflareZone>, Option<PaginationInfo>), String> {
382        let envelope: CloudflareEnvelope<Vec<CloudflareZone>> = self
383            .http
384            .get_json("/zones", Some(filters))
385            .await
386            .map_err(render_http_error)?;
387        Ok((envelope.result, envelope.result_info))
388    }
389
390    pub async fn get_zone(&self, zone_id: &str) -> Result<CloudflareZone, String> {
391        let envelope: CloudflareEnvelope<CloudflareZone> = self
392            .http
393            .get_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
394            .await
395            .map_err(render_http_error)?;
396        Ok(envelope.result)
397    }
398
399    pub async fn create_zone(
400        &self,
401        request: &CloudflareZoneCreateRequest,
402    ) -> Result<CloudflareZone, String> {
403        let envelope: CloudflareEnvelope<CloudflareZone> = self
404            .http
405            .post_json("/zones", request)
406            .await
407            .map_err(render_http_error)?;
408        Ok(envelope.result)
409    }
410
411    pub async fn edit_zone(
412        &self,
413        zone_id: &str,
414        request: &CloudflareZoneEditRequest,
415    ) -> Result<CloudflareZone, String> {
416        let envelope: CloudflareEnvelope<CloudflareZone> = self
417            .http
418            .patch_json(&format!("/zones/{}", zone_id), request)
419            .await
420            .map_err(render_http_error)?;
421        Ok(envelope.result)
422    }
423
424    pub async fn delete_zone(&self, zone_id: &str) -> Result<(), String> {
425        let _: CloudflareEnvelope<Option<Value>> = self
426            .http
427            .delete_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
428            .await
429            .map_err(render_http_error)?;
430        Ok(())
431    }
432
433    pub async fn list_records(
434        &self,
435        zone_id: &str,
436        filters: &CloudflareDnsRecordListFilters,
437    ) -> Result<(Vec<CloudflareDnsRecord>, Option<PaginationInfo>), String> {
438        let envelope: CloudflareEnvelope<Vec<CloudflareDnsRecord>> = self
439            .http
440            .get_json(&format!("/zones/{}/dns_records", zone_id), Some(filters))
441            .await
442            .map_err(render_http_error)?;
443        Ok((envelope.result, envelope.result_info))
444    }
445
446    pub async fn get_record(
447        &self,
448        zone_id: &str,
449        record_id: &str,
450    ) -> Result<CloudflareDnsRecord, String> {
451        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
452            .http
453            .get_json(
454                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
455                Option::<&Value>::None,
456            )
457            .await
458            .map_err(render_http_error)?;
459        Ok(envelope.result)
460    }
461
462    pub async fn create_record(
463        &self,
464        zone_id: &str,
465        request: &CloudflareDnsRecordWrite,
466    ) -> Result<CloudflareDnsRecord, String> {
467        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
468            .http
469            .post_json(&format!("/zones/{}/dns_records", zone_id), request)
470            .await
471            .map_err(render_http_error)?;
472        Ok(envelope.result)
473    }
474
475    pub async fn replace_record(
476        &self,
477        zone_id: &str,
478        record_id: &str,
479        request: &CloudflareDnsRecordWrite,
480    ) -> Result<CloudflareDnsRecord, String> {
481        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
482            .http
483            .put_json(
484                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
485                request,
486            )
487            .await
488            .map_err(render_http_error)?;
489        Ok(envelope.result)
490    }
491
492    pub async fn edit_record(
493        &self,
494        zone_id: &str,
495        record_id: &str,
496        request: &CloudflareDnsRecordWrite,
497    ) -> Result<CloudflareDnsRecord, String> {
498        let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
499            .http
500            .patch_json(
501                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
502                request,
503            )
504            .await
505            .map_err(render_http_error)?;
506        Ok(envelope.result)
507    }
508
509    pub async fn delete_record(&self, zone_id: &str, record_id: &str) -> Result<(), String> {
510        let _: CloudflareEnvelope<Option<Value>> = self
511            .http
512            .delete_json(
513                &format!("/zones/{}/dns_records/{}", zone_id, record_id),
514                Option::<&Value>::None,
515            )
516            .await
517            .map_err(render_http_error)?;
518        Ok(())
519    }
520
521    pub async fn batch_records(
522        &self,
523        zone_id: &str,
524        request: &CloudflareDnsRecordBatch,
525    ) -> Result<Value, String> {
526        let envelope: CloudflareEnvelope<Value> = self
527            .http
528            .post_json(&format!("/zones/{}/dns_records/batch", zone_id), request)
529            .await
530            .map_err(render_http_error)?;
531        Ok(envelope.result)
532    }
533
534    pub async fn export_records(&self, zone_id: &str) -> Result<ResponseBytes, String> {
535        self.http
536            .get_bytes(
537                &format!("/zones/{}/dns_records/export", zone_id),
538                Option::<&Value>::None,
539            )
540            .await
541            .map_err(render_http_error)
542    }
543
544    pub async fn import_records(&self, zone_id: &str, bytes: Vec<u8>) -> Result<Value, String> {
545        let response = self
546            .http
547            .post_bytes(
548                &format!("/zones/{}/dns_records/import", zone_id),
549                bytes,
550                "text/plain",
551            )
552            .await
553            .map_err(render_http_error)?;
554        let envelope: CloudflareEnvelope<Value> =
555            serde_json::from_slice(&response.body).map_err(|error| error.to_string())?;
556        Ok(envelope.result)
557    }
558
559    pub async fn get_dnssec(&self, zone_id: &str) -> Result<CloudflareDnssec, String> {
560        let envelope: CloudflareEnvelope<CloudflareDnssec> = self
561            .http
562            .get_json(
563                &format!("/zones/{}/dnssec", zone_id),
564                Option::<&Value>::None,
565            )
566            .await
567            .map_err(render_http_error)?;
568        Ok(envelope.result)
569    }
570
571    pub async fn edit_dnssec(
572        &self,
573        zone_id: &str,
574        request: &CloudflareDnssecEdit,
575    ) -> Result<CloudflareDnssec, String> {
576        let envelope: CloudflareEnvelope<CloudflareDnssec> = self
577            .http
578            .patch_json(&format!("/zones/{}/dnssec", zone_id), request)
579            .await
580            .map_err(render_http_error)?;
581        Ok(envelope.result)
582    }
583
584    pub async fn get_dns_settings(&self, zone_id: &str) -> Result<CloudflareDnsSettings, String> {
585        let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
586            .http
587            .get_json(
588                &format!("/zones/{}/dns_settings", zone_id),
589                Option::<&Value>::None,
590            )
591            .await
592            .map_err(render_http_error)?;
593        Ok(envelope.result)
594    }
595
596    pub async fn edit_dns_settings(
597        &self,
598        zone_id: &str,
599        request: &CloudflareDnsSettings,
600    ) -> Result<CloudflareDnsSettings, String> {
601        let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
602            .http
603            .patch_json(&format!("/zones/{}/dns_settings", zone_id), request)
604            .await
605            .map_err(render_http_error)?;
606        Ok(envelope.result)
607    }
608}
609
610fn render_http_error(error: HttpError) -> String {
611    error.to_string()
612}