Skip to content

Commit e33170b

Browse files
committed
fix: update cloudflare crate
1 parent d90da47 commit e33170b

File tree

7 files changed

+189
-124
lines changed

7 files changed

+189
-124
lines changed

svc/Cargo.lock

Lines changed: 5 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

svc/pkg/cluster/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ license = "Apache-2.0"
99
acme-lib = "0.9"
1010
anyhow = "1.0"
1111
chirp-workflow = { path = "../../../lib/chirp-workflow/core" }
12-
cloudflare = "0.10.1"
12+
cloudflare = { git = "https://github.com/cloudflare/cloudflare-rs.git", rev = "f14720e42184ee176a97676e85ef2d2d85bc3aae" }
1313
http = "0.2"
1414
include_dir = "0.7.3"
1515
indoc = "1.0"
1616
lazy_static = "1.4"
1717
nomad-util = { path = "../../../lib/nomad-util" }
1818
rand = "0.8"
19+
reqwest = { version = "0.11", features = ["json"] }
1920
rivet-metrics = { path = "../../../lib/metrics" }
2021
rivet-operation = { path = "../../../lib/operation/core" }
2122
rivet-runtime = { path = "../../../lib/runtime" }
2223
s3-util = { path = "../../../lib/s3-util" }
2324
serde = { version = "1.0.198", features = ["derive"] }
2425
ssh2 = "0.9.4"
25-
thiserror = "1.0"
2626
trust-dns-resolver = { version = "0.23.2", features = ["dns-over-native-tls"] }
2727

2828
ip-info = { path = "../ip/ops/info" }

svc/pkg/cluster/src/util/mod.rs

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use chirp_workflow::prelude::*;
2-
use cloudflare::framework as cf_framework;
2+
use cloudflare::{endpoints as cf, framework as cf_framework};
33

44
use 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
1414
pub 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
2417
pub 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

Comments
 (0)