Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/fakecloud-athena/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub(crate) mod service;
pub(crate) mod sql;
pub(crate) mod state;

pub use service::AthenaService;
pub use service::{athena_arn, AthenaService};
pub use state::{
AthenaAccounts, AthenaSnapshot, DataCatalog, NamedQuery, PreparedStatement, SharedAthenaState,
WorkGroup, ATHENA_SNAPSHOT_SCHEMA_VERSION,
Expand Down
6 changes: 5 additions & 1 deletion crates/fakecloud-athena/src/service/capacity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ impl AthenaService {
last_allocation: Some(Utc::now()),
last_successful_allocation_time: Some(Utc::now()),
};
let arn = capacity_reservation_arn(&req.account_id, &req.region, &name);
let arn = athena_arn(
&req.region,
&req.account_id,
&format!("capacity-reservation/{name}"),
);
account.capacity_reservations.insert(name, cr);
if !tags.is_empty() {
account.tags.insert(arn, tags);
Expand Down
2 changes: 1 addition & 1 deletion crates/fakecloud-athena/src/service/data_catalogs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl AthenaService {
connection_type,
error: None,
};
let arn = datacatalog_arn(&req.account_id, &req.region, &name);
let arn = athena_arn(&req.region, &req.account_id, &format!("datacatalog/{name}"));
account.data_catalogs.insert(name, cat);
if !tags.is_empty() {
account.tags.insert(arn, tags);
Expand Down
69 changes: 43 additions & 26 deletions crates/fakecloud-athena/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,37 +658,16 @@ fn synth_uuid() -> String {
Uuid::new_v4().to_string()
}

fn workgroup_arn(account_id: &str, region: &str, name: &str) -> String {
/// An Athena ARN (`resource` is e.g. `workgroup/<name>`,
/// `datacatalog/<name>` or `capacity-reservation/<name>`), in `region`'s
/// partition. An empty region falls back to `us-east-1`.
pub fn athena_arn(region: &str, account_id: &str, resource: &str) -> String {
let region = if region.is_empty() {
"us-east-1"
} else {
region
};
Arn::new("athena", region, account_id, &format!("workgroup/{name}")).to_string()
}

fn datacatalog_arn(account_id: &str, region: &str, name: &str) -> String {
let region = if region.is_empty() {
"us-east-1"
} else {
region
};
Arn::new("athena", region, account_id, &format!("datacatalog/{name}")).to_string()
}

fn capacity_reservation_arn(account_id: &str, region: &str, name: &str) -> String {
let region = if region.is_empty() {
"us-east-1"
} else {
region
};
Arn::new(
"athena",
region,
account_id,
&format!("capacity-reservation/{name}"),
)
.to_string()
Arn::regional("athena", region, account_id, resource).to_string()
}

fn parse_string_list(value: Option<&Value>) -> Vec<String> {
Expand Down Expand Up @@ -2004,6 +1983,44 @@ mod tests {
assert_eq!(deleted["DataCatalog"]["Type"], "GLUE");
assert_eq!(deleted["DataCatalog"]["Status"], "DELETE_COMPLETE");
}

/// Athena ARNs take the partition of the region they are minted in, so a
/// resource created in a China region is tagged and looked up under its
/// `aws-cn` ARN.
#[test]
fn arns_use_the_regions_partition() {
assert_eq!(
super::athena_arn("cn-north-1", "123456789012", "workgroup/wg"),
"arn:aws-cn:athena:cn-north-1:123456789012:workgroup/wg"
);
assert_eq!(
super::athena_arn("cn-north-1", "123456789012", "datacatalog/cat"),
"arn:aws-cn:athena:cn-north-1:123456789012:datacatalog/cat"
);
assert_eq!(
super::athena_arn("us-gov-west-1", "123456789012", "capacity-reservation/cr"),
"arn:aws-us-gov:athena:us-gov-west-1:123456789012:capacity-reservation/cr"
);
assert_eq!(
super::athena_arn("us-east-1", "123456789012", "workgroup/wg"),
"arn:aws:athena:us-east-1:123456789012:workgroup/wg"
);

let svc = AthenaService::new(SharedAthenaState::default());
let mut create = req(
"CreateWorkGroup",
json!({ "Name": "cnwg", "Tags": [{ "Key": "team", "Value": "data" }] }),
);
create.region = "cn-north-1".to_string();
svc.create_work_group(&create).unwrap();
let mut list = req(
"ListTagsForResource",
json!({ "ResourceARN": "arn:aws-cn:athena:cn-north-1:123456789012:workgroup/cnwg" }),
);
list.region = "cn-north-1".to_string();
let tags = parse_json(&svc.list_tags_for_resource(&list).unwrap());
assert_eq!(tags["Tags"], json!([{ "Key": "team", "Value": "data" }]));
}
}

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/fakecloud-athena/src/service/work_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl AthenaService {
creation_time: Utc::now(),
engine_version,
};
let arn = workgroup_arn(&req.account_id, &req.region, &name);
let arn = athena_arn(&req.region, &req.account_id, &format!("workgroup/{name}"));
account.work_groups.insert(name, wg);
if !tags.is_empty() {
account.tags.insert(arn, tags);
Expand Down
84 changes: 72 additions & 12 deletions crates/fakecloud-bedrock/src/prompt_routers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ pub(crate) fn get_prompt_router(
"promptRouterArn": router.prompt_router_arn,
"promptRouterName": router.prompt_router_name,
"description": router.description,
"models": ensure_router_models(&router.models),
"models": ensure_router_models(&router.models, &router.prompt_router_arn, &req.region),
"routingCriteria": ensure_routing_criteria(&router.routing_criteria),
"fallbackModel": ensure_fallback_model(&router.fallback_model),
"fallbackModel": ensure_fallback_model(&router.fallback_model, &router.prompt_router_arn, &req.region),
"status": router.status,
"type": router.prompt_router_type,
"createdAt": router.created_at.to_rfc3339(),
Expand Down Expand Up @@ -118,8 +118,8 @@ pub(crate) fn list_prompt_routers(
"promptRouterName": r.prompt_router_name,
"description": r.description,
"routingCriteria": ensure_routing_criteria(&r.routing_criteria),
"models": ensure_router_models(&r.models),
"fallbackModel": ensure_fallback_model(&r.fallback_model),
"models": ensure_router_models(&r.models, &r.prompt_router_arn, &req.region),
"fallbackModel": ensure_fallback_model(&r.fallback_model, &r.prompt_router_arn, &req.region),
"status": r.status,
"type": r.prompt_router_type,
"createdAt": r.created_at.to_rfc3339(),
Expand Down Expand Up @@ -148,28 +148,37 @@ fn ensure_routing_criteria(criteria: &Value) -> Value {
json!({ "responseQualityDifference": 0.0 })
}

/// The foundation model a router defaults to, in the router's own region and
/// partition. The router's stored ARN names that region; `request_region` is
/// the source of truth only when the ARN does not parse.
fn default_router_model_arn(router_arn: &str, request_region: &str) -> String {
let parsed = router_arn.parse::<fakecloud_aws::arn::Arn>().ok();
let region = parsed
.as_ref()
.map(|a| a.region.as_str())
.filter(|r| !r.is_empty())
.unwrap_or(request_region);
crate::arns::foundation_model_arn(region, "amazon.titan-text-express-v1")
}

/// Default the router's target models to a single Titan entry when none were
/// supplied at create time, matching the required Smithy field.
fn ensure_router_models(models: &Value) -> Value {
fn ensure_router_models(models: &Value, router_arn: &str, request_region: &str) -> Value {
if let Some(arr) = models.as_array() {
if !arr.is_empty() {
return models.clone();
}
}
json!([{
"modelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1"
}])
json!([{ "modelArn": default_router_model_arn(router_arn, request_region) }])
}

/// Default the fallback model when not provided. `fallbackModel` is required
/// in the summary even though it's optional on create.
fn ensure_fallback_model(fallback: &Value) -> Value {
fn ensure_fallback_model(fallback: &Value, router_arn: &str, request_region: &str) -> Value {
if fallback.get("modelArn").is_some() {
return fallback.clone();
}
json!({
"modelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1"
})
json!({ "modelArn": default_router_model_arn(router_arn, request_region) })
}

pub(crate) fn delete_prompt_router(
Expand Down Expand Up @@ -274,6 +283,57 @@ mod tests {
assert!(get_prompt_router(&s, &req(), "my-router").is_ok());
}

/// A router created without models defaults them to a foundation model in
/// the router's own region and partition.
#[test]
fn default_models_use_the_routers_partition() {
let s = shared();
let mut r = req();
r.region = "cn-north-1".to_string();
let resp = create_prompt_router(&s, &r, &json!({ "promptRouterName": "cn" })).unwrap();
let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
let arn = v["promptRouterArn"].as_str().unwrap().to_string();
assert!(arn.starts_with("arn:aws-cn:bedrock:cn-north-1:"), "{arn}");
let got = get_prompt_router(&s, &r, &arn).unwrap();
let g: Value = serde_json::from_slice(got.body.expect_bytes()).unwrap();
let titan = "arn:aws-cn:bedrock:cn-north-1::foundation-model/amazon.titan-text-express-v1";
assert_eq!(g["models"], json!([{ "modelArn": titan }]));
assert_eq!(g["fallbackModel"], json!({ "modelArn": titan }));
let listed = list_prompt_routers(&s, &r).unwrap();
let l: Value = serde_json::from_slice(listed.body.expect_bytes()).unwrap();
assert_eq!(
l["promptRouterSummaries"][0]["fallbackModel"]["modelArn"],
titan
);

// Commercial output is unchanged.
let arn = create_prompt_router(&s, &req(), &json!({ "promptRouterName": "us" })).unwrap();
let v: Value = serde_json::from_slice(arn.body.expect_bytes()).unwrap();
let got = get_prompt_router(&s, &req(), v["promptRouterArn"].as_str().unwrap()).unwrap();
let g: Value = serde_json::from_slice(got.body.expect_bytes()).unwrap();
assert_eq!(
g["fallbackModel"]["modelArn"],
"arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-text-express-v1"
);
}

/// The router's stored ARN names the region; only an ARN that does not
/// parse falls back to the request's region, never to a fixed one.
#[test]
fn default_model_region_comes_from_the_router_arn_then_the_request() {
assert_eq!(
default_router_model_arn(
"arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:prompt-router/x",
"cn-north-1"
),
"arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/amazon.titan-text-express-v1"
);
assert_eq!(
default_router_model_arn("not-an-arn", "cn-north-1"),
"arn:aws-cn:bedrock:cn-north-1::foundation-model/amazon.titan-text-express-v1"
);
}

#[test]
fn get_unknown_returns_not_found() {
let s = shared();
Expand Down
6 changes: 3 additions & 3 deletions crates/fakecloud-cloudwatch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod tests;
pub use delivery::CloudwatchDeliveryImpl;
pub use service::CloudWatchService;
pub use state::{
AlarmMetricQuery, AlarmMetricStat, AlarmState, CloudWatchAccounts, CloudWatchSnapshot,
CloudWatchState, Dashboard, MetricAlarm, MetricDatum, SharedCloudWatchState,
CLOUDWATCH_SNAPSHOT_SCHEMA_VERSION,
alarm_arn, AlarmMetricQuery, AlarmMetricStat, AlarmState, CloudWatchAccounts,
CloudWatchSnapshot, CloudWatchState, Dashboard, MetricAlarm, MetricDatum,
SharedCloudWatchState, CLOUDWATCH_SNAPSHOT_SCHEMA_VERSION,
};
2 changes: 1 addition & 1 deletion crates/fakecloud-cloudwatch/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ pub struct AlarmHistoryItem {

/// The ARN of alarm `name` (metric, composite or log alarm) in `region`'s
/// partition.
pub(crate) fn alarm_arn(region: &str, account_id: &str, name: &str) -> String {
pub fn alarm_arn(region: &str, account_id: &str, name: &str) -> String {
fakecloud_aws::arn::Arn::regional("cloudwatch", region, account_id, &format!("alarm:{name}"))
.to_string()
}
Expand Down
1 change: 1 addition & 0 deletions crates/fakecloud-efs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ tracing = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
fakecloud-kms = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
bytes = { workspace = true }
45 changes: 41 additions & 4 deletions crates/fakecloud-efs/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,16 +675,21 @@ impl EfsService {
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| {
format!(
"arn:aws:kms:{}:{}:key/{}-{}-{}-{}-{}",
ctx.region,
ctx.account,
let key_id = format!(
"{}-{}-{}-{}-{}",
&hex17()[..8],
&hex17()[..4],
&hex17()[..4],
&hex17()[..4],
&hex17()[..12.min(hex17().len())]
);
fakecloud_aws::arn::Arn::regional(
"kms",
&ctx.region,
&ctx.account,
&format!("key/{key_id}"),
)
.to_string()
});
fs.insert("KmsKeyId".into(), json!(kms));
}
Expand Down Expand Up @@ -1888,6 +1893,38 @@ mod tests {
);
}

/// An encrypted file system's default key is the ARN KMS itself mints for
/// that key id in the caller's region, partition included.
#[test]
fn default_kms_key_uses_the_regions_partition() {
let s = svc();
let c = Ctx {
account: "000000000000".to_string(),
region: "cn-north-1".to_string(),
};
let fs = body_value(
&s.create_file_system(&c, &json!({ "CreationToken": "enc", "Encrypted": true }))
.unwrap(),
);
let kms = fs["KmsKeyId"].as_str().unwrap();
let key_id = kms
.strip_prefix("arn:aws-cn:kms:cn-north-1:000000000000:key/")
.unwrap_or_else(|| panic!("{kms}"));
assert_eq!(
kms,
fakecloud_kms::kms_key_arn("cn-north-1", "000000000000", key_id)
);
// Commercial output keeps the `aws` partition.
let fs = body_value(
&s.create_file_system(&ctx(), &json!({ "CreationToken": "us", "Encrypted": true }))
.unwrap(),
);
assert!(fs["KmsKeyId"]
.as_str()
.unwrap()
.starts_with("arn:aws:kms:us-east-1:000000000000:key/"));
}

// Defect #1: a nonexistent subnet must be rejected with SubnetNotFound once
// EC2 state is wired to validate against (rather than synthesizing one).
#[test]
Expand Down
5 changes: 3 additions & 2 deletions crates/fakecloud-eventbridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) mod state;
pub use service::helpers::parse_target;
pub use service::EventBridgeService;
pub use state::{
ApiDestination, Archive, Connection, Endpoint, EventBridgeSnapshot, EventBridgeState, EventBus,
EventRule, EventTarget, SharedEventBridgeState, EVENTBRIDGE_SNAPSHOT_SCHEMA_VERSION,
bus_arn, rule_arn, ApiDestination, Archive, Connection, Endpoint, EventBridgeSnapshot,
EventBridgeState, EventBus, EventRule, EventTarget, SharedEventBridgeState,
EVENTBRIDGE_SNAPSHOT_SCHEMA_VERSION,
};
Loading
Loading