11use chirp_workflow:: prelude:: * ;
2- use cloudflare:: framework as cf_framework;
2+ use cloudflare:: { endpoints as cf , framework as cf_framework} ;
33
44use crate :: types:: PoolType ;
55
@@ -13,13 +13,6 @@ pub const INSTALL_SCRIPT_HASH: &str = include_str!(concat!(env!("OUT_DIR"), "/ha
1313// TTL of the token written to prebake images. Prebake images are renewed before the token would expire
1414pub const SERVER_TOKEN_TTL : i64 = util:: duration:: days ( 30 * 6 ) ;
1515
16- #[ derive( thiserror:: Error , Debug ) ]
17- #[ error( "cloudflare: {source}" ) ]
18- struct CloudflareError {
19- #[ from]
20- source : anyhow:: Error ,
21- }
22-
2316// Cluster id for provisioning servers
2417pub fn default_cluster_id ( ) -> Uuid {
2518 Uuid :: nil ( )
@@ -36,15 +29,145 @@ pub fn server_name(provider_datacenter_id: &str, pool_type: PoolType, server_id:
3629 format ! ( "{ns}-{provider_datacenter_id}-{pool_type_str}-{server_id}" , )
3730}
3831
39- pub ( crate ) async fn cf_client ( ) -> GlobalResult < cf_framework:: async_api:: Client > {
32+ pub ( crate ) async fn cf_client (
33+ cf_token : Option < & str > ,
34+ ) -> GlobalResult < cf_framework:: async_api:: Client > {
4035 // Create CF client
41- let cf_token = util:: env:: read_secret ( & [ "cloudflare" , "terraform" , "auth_token" ] ) . await ?;
36+ let cf_token = if let Some ( cf_token) = cf_token {
37+ cf_token. to_string ( )
38+ } else {
39+ util:: env:: read_secret ( & [ "cloudflare" , "terraform" , "auth_token" ] ) . await ?
40+ } ;
4241 let client = cf_framework:: async_api:: Client :: new (
4342 cf_framework:: auth:: Credentials :: UserAuthToken { token : cf_token } ,
4443 Default :: default ( ) ,
4544 cf_framework:: Environment :: Production ,
46- )
47- . map_err ( CloudflareError :: from) ?;
45+ ) ?;
4846
4947 Ok ( client)
5048}
49+
50+ /// Tries to create a DNS record. If a 400 error is received, it deletes the existing record and tries again.
51+ pub ( crate ) async fn create_dns_record (
52+ client : & cf_framework:: async_api:: Client ,
53+ cf_token : & str ,
54+ zone_id : & str ,
55+ record_name : & str ,
56+ content : cf:: dns:: DnsContent ,
57+ ) -> GlobalResult < String > {
58+ tracing:: info!( %record_name, "creating dns record" ) ;
59+
60+ let create_record_res = client
61+ . request ( & cf:: dns:: CreateDnsRecord {
62+ zone_identifier : zone_id,
63+ params : cf:: dns:: CreateDnsRecordParams {
64+ name : record_name,
65+ content : content. clone ( ) ,
66+ proxied : Some ( false ) ,
67+ ttl : Some ( 60 ) ,
68+ priority : None ,
69+ } ,
70+ } )
71+ . await ;
72+
73+ match create_record_res {
74+ Ok ( create_record_res) => Ok ( create_record_res. result . id ) ,
75+ // Try to delete record on error
76+ Err ( err) => {
77+ if let cf_framework:: response:: ApiFailure :: Error (
78+ http:: status:: StatusCode :: BAD_REQUEST ,
79+ _,
80+ ) = err
81+ {
82+ tracing:: warn!( %record_name, "failed to create dns record, trying to delete" ) ;
83+
84+ let dns_type = match content {
85+ cf:: dns:: DnsContent :: A { .. } => "A" ,
86+ cf:: dns:: DnsContent :: AAAA { .. } => "AAAA" ,
87+ cf:: dns:: DnsContent :: CNAME { .. } => "CNAME" ,
88+ cf:: dns:: DnsContent :: NS { .. } => "NS" ,
89+ cf:: dns:: DnsContent :: MX { .. } => "MX" ,
90+ cf:: dns:: DnsContent :: TXT { .. } => "TXT" ,
91+ cf:: dns:: DnsContent :: SRV { .. } => "SRV" ,
92+ } ;
93+ let list_records_res = get_dns_record ( cf_token, record_name, dns_type) . await ?;
94+
95+ if let Some ( record) = list_records_res {
96+ delete_dns_record ( client, zone_id, & record. id ) . await ?;
97+ tracing:: info!( %record_name, "deleted dns record, trying again" ) ;
98+
99+ // Second try
100+ let create_record_res2 = client
101+ . request ( & cf:: dns:: CreateDnsRecord {
102+ zone_identifier : zone_id,
103+ params : cf:: dns:: CreateDnsRecordParams {
104+ name : record_name,
105+ content,
106+ proxied : Some ( false ) ,
107+ ttl : Some ( 60 ) ,
108+ priority : None ,
109+ } ,
110+ } )
111+ . await ?;
112+
113+ return Ok ( create_record_res2. result . id ) ;
114+ } else {
115+ tracing:: warn!( %record_name, "failed to get matching dns record" ) ;
116+ }
117+ }
118+
119+ // Throw original error
120+ Err ( err. into ( ) )
121+ }
122+ }
123+ }
124+
125+ pub ( crate ) async fn delete_dns_record (
126+ client : & cf_framework:: async_api:: Client ,
127+ zone_id : & str ,
128+ record_id : & str ,
129+ ) -> GlobalResult < ( ) > {
130+ tracing:: info!( %record_id, "deleting dns record" ) ;
131+
132+ client
133+ . request ( & cf:: dns:: DeleteDnsRecord {
134+ zone_identifier : zone_id,
135+ identifier : record_id,
136+ } )
137+ . await ?;
138+
139+ Ok ( ( ) )
140+ }
141+
142+ /// Fetches the dns record by name.
143+ async fn get_dns_record (
144+ cf_token : & str ,
145+ record_name : & str ,
146+ dns_type : & str ,
147+ ) -> GlobalResult < Option < cf:: dns:: DnsRecord > > {
148+ let list_records_res = reqwest:: Client :: new ( )
149+ . get ( "https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" )
150+ . bearer_auth ( cf_token)
151+ . query ( & ( "name" , & record_name) )
152+ . query ( & ( "type" , dns_type) )
153+ . send ( )
154+ . await ?
155+ . to_global_error ( )
156+ . await ?;
157+
158+ let status = list_records_res. status ( ) ;
159+ if status. is_success ( ) {
160+ match list_records_res
161+ . json :: < cf_framework:: response:: ApiSuccess < Vec < cf:: dns:: DnsRecord > > > ( )
162+ . await
163+ {
164+ Ok ( api_resp) => Ok ( api_resp. result . into_iter ( ) . next ( ) ) ,
165+ Err ( e) => Err ( cf_framework:: response:: ApiFailure :: Invalid ( e) . into ( ) ) ,
166+ }
167+ } else {
168+ let parsed: Result < cf_framework:: response:: ApiErrors , reqwest:: Error > =
169+ list_records_res. json ( ) . await ;
170+ let errors = parsed. unwrap_or_default ( ) ;
171+ Err ( cf_framework:: response:: ApiFailure :: Error ( status, errors) . into ( ) )
172+ }
173+ }
0 commit comments