Skip to content

Commit fe9b219

Browse files
committed
fix(validator): accept new AWS regions like eu-central-2
rusoto_core 0.48.0 (abandoned) hardcodes the set of known AWS regions and rejects anything launched after its last release, surfacing as "Not a valid AWS region: eu-central-2" when parsing the validator's checkpointSyncer config or the relayer's S3 storage location URL. Replace the rusoto-based region validation with a format check (regex) that accepts any well-formed AWS region string, so newer regions (eu-central-2, ap-south-2, il-central-1, mx-central-1, ca-west-1, ...) work without waiting on rusoto updates that will never come.
1 parent e63aeea commit fe9b219

4 files changed

Lines changed: 96 additions & 16 deletions

File tree

rust/main/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/main/agents/validator/src/settings.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use hyperlane_base::{
1313
impl_loadable_from_settings,
1414
settings::{
1515
parser::{RawAgentConf, RawAgentSignerConf, ValueParser},
16-
CheckpointSyncerConf, Settings, SignerConf,
16+
validate_aws_region, CheckpointSyncerConf, Settings, SignerConf,
1717
},
1818
};
1919
use hyperlane_core::{
@@ -293,12 +293,12 @@ fn parse_checkpoint_syncer(syncer: ValueParser) -> ConfigResult<CheckpointSyncer
293293
.parse_string()
294294
.end()
295295
.map(str::to_owned);
296-
// Using rusoto_core::Region just to get some input validation
297-
let region: Option<rusoto_core::Region> = syncer
296+
let region: Option<String> = syncer
298297
.chain(&mut err)
299298
.get_key("region")
300-
.parse_from_str("Expected aws region")
301-
.end();
299+
.parse_string()
300+
.end()
301+
.map(str::to_owned);
302302
let folder = syncer
303303
.chain(&mut err)
304304
.get_opt_key("folder")
@@ -307,9 +307,13 @@ fn parse_checkpoint_syncer(syncer: ValueParser) -> ConfigResult<CheckpointSyncer
307307
.map(str::to_owned);
308308

309309
cfg_unwrap_all!(&syncer.cwp, err: [bucket, region]);
310+
if let Err(e) = validate_aws_region(&region) {
311+
err.push(syncer.cwp.join("region"), e);
312+
return Err(err);
313+
}
310314
err.into_result(CheckpointSyncerConf::S3 {
311315
bucket,
312-
region: Region::new(region.name().to_owned()),
316+
region: Region::new(region),
313317
folder,
314318
})
315319
}

rust/main/hyperlane-base/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ maplit.workspace = true
3131
mockall.workspace = true
3232
paste.workspace = true
3333
prometheus.workspace = true
34+
regex.workspace = true
3435
rocksdb.workspace = true
3536
serde.workspace = true
3637
serde_json.workspace = true

rust/main/hyperlane-base/src/settings/checkpoint_syncer.rs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use std::{env, path::PathBuf};
1+
use std::{env, path::PathBuf, sync::OnceLock};
22

33
use aws_config::Region;
44
use core::str::FromStr;
55
use eyre::{eyre, Context, Report, Result};
66
use prometheus::IntGauge;
7+
use regex::Regex;
78
use tracing::error;
89
use ya_gcp::{AuthFlow, ServiceAccountAuth};
910

@@ -14,6 +15,23 @@ use crate::{
1415
GCS_USER_SECRET,
1516
};
1617

18+
/// Validates an AWS region string by format.
19+
///
20+
/// The canonical list is maintained by AWS
21+
/// (https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html)
22+
/// and grows over time, so we validate by shape rather than against a hardcoded
23+
/// allowlist — this is why `rusoto_core::Region::from_str` (abandoned crate) was
24+
/// rejecting valid regions like `eu-central-2`.
25+
pub fn validate_aws_region(region: &str) -> Result<()> {
26+
static REGION_RE: OnceLock<Regex> = OnceLock::new();
27+
let re = REGION_RE
28+
.get_or_init(|| Regex::new(r"^[a-z]{2,3}(-[a-z]+)+-\d+$").expect("valid region regex"));
29+
if !re.is_match(region) {
30+
return Err(eyre!("Not a valid AWS region: {region}"));
31+
}
32+
Ok(())
33+
}
34+
1735
/// Checkpoint Syncer types
1836
#[derive(Debug, Clone)]
1937
pub enum CheckpointSyncerConf {
@@ -76,18 +94,12 @@ impl FromStr for CheckpointSyncerConf {
7694
3 .. => Ok((url_components[0], url_components[1], Some(url_components[2..].join("/")))),
7795
_ => Err(eyre!("Error parsing storage location; could not split bucket, region and folder ({suffix})"))
7896
}?;
97+
validate_aws_region(region)
98+
.context("Invalid region when parsing storage location")?;
7999
Ok(CheckpointSyncerConf::S3 {
80100
bucket: bucket.into(),
81101
folder,
82-
// Wildly, aws_config doesn't provide any client-side way to validate a region string, so while
83-
// we still have Rusoto around we just use that to validate the region string :)
84-
region: aws_config::Region::new(
85-
region
86-
.parse::<rusoto_core::Region>()
87-
.context("Invalid region when parsing storage location")?
88-
.name()
89-
.to_owned(),
90-
),
102+
region: aws_config::Region::new(region.to_owned()),
91103
})
92104
}
93105
"file" => Ok(CheckpointSyncerConf::LocalStorage {
@@ -290,4 +302,66 @@ mod test {
290302
_ => panic!("Expected a reorg event error"),
291303
}
292304
}
305+
306+
#[test]
307+
fn test_validate_aws_region() {
308+
use super::validate_aws_region;
309+
310+
for region in [
311+
"us-east-1",
312+
"us-east-2",
313+
"us-west-2",
314+
"eu-central-1",
315+
"eu-central-2",
316+
"eu-west-1",
317+
"eu-north-1",
318+
"ap-southeast-1",
319+
"ap-southeast-7",
320+
"ap-south-2",
321+
"ca-west-1",
322+
"il-central-1",
323+
"me-central-1",
324+
"mx-central-1",
325+
"us-gov-east-1",
326+
"us-iso-east-1",
327+
] {
328+
assert!(
329+
validate_aws_region(region).is_ok(),
330+
"expected {region} to be accepted"
331+
);
332+
}
333+
334+
for region in [
335+
"",
336+
"US-EAST-1",
337+
"us_east_1",
338+
"us-east",
339+
"useast1",
340+
"us-east-1a",
341+
"foo",
342+
] {
343+
assert!(
344+
validate_aws_region(region).is_err(),
345+
"expected {region} to be rejected"
346+
);
347+
}
348+
}
349+
350+
#[test]
351+
fn test_parse_s3_storage_location_with_new_region() {
352+
use super::*;
353+
let conf = CheckpointSyncerConf::from_str("s3://my-bucket/eu-central-2/folder").unwrap();
354+
match conf {
355+
CheckpointSyncerConf::S3 {
356+
bucket,
357+
folder,
358+
region,
359+
} => {
360+
assert_eq!(bucket, "my-bucket");
361+
assert_eq!(folder.as_deref(), Some("folder"));
362+
assert_eq!(region.as_ref(), "eu-central-2");
363+
}
364+
_ => panic!("Expected S3 checkpoint syncer"),
365+
}
366+
}
293367
}

0 commit comments

Comments
 (0)