use fakecloud_aws::ec2query::{ec2_elem, ec2_list, ec2_return};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::service::Ec2Service;
use crate::service_helpers::{
gen_id, indexed_list, parse_filters, require, validate_enum, validate_int_range,
validate_max_results, Filter,
};
use crate::state::{
Ec2State, InternetGateway, NatGateway, Route, RouteTable, RouteTableAssociation, Tag,
};
const FIXED_TIME: &str = "2024-01-01T00:00:00.000Z";
fn route_xml(r: &Route) -> String {
let mut out = String::new();
let opt = |out: &mut String, name: &str, v: &Option<String>| {
if let Some(v) = v {
out.push_str(&ec2_elem(name, v));
}
};
opt(&mut out, "destinationCidrBlock", &r.destination_cidr_block);
opt(
&mut out,
"destinationIpv6CidrBlock",
&r.destination_ipv6_cidr_block,
);
opt(
&mut out,
"destinationPrefixListId",
&r.destination_prefix_list_id,
);
opt(&mut out, "gatewayId", &r.gateway_id);
opt(&mut out, "natGatewayId", &r.nat_gateway_id);
opt(&mut out, "networkInterfaceId", &r.network_interface_id);
opt(&mut out, "instanceId", &r.instance_id);
opt(
&mut out,
"vpcPeeringConnectionId",
&r.vpc_peering_connection_id,
);
opt(&mut out, "transitGatewayId", &r.transit_gateway_id);
opt(
&mut out,
"egressOnlyInternetGatewayId",
&r.egress_only_internet_gateway_id,
);
out.push_str(&ec2_elem("state", &r.state));
out.push_str(&ec2_elem("origin", &r.origin));
out
}
fn rt_xml(rt: &RouteTable, tags: &[Tag], owner: &str) -> String {
let routes: Vec<String> = rt.routes.iter().map(route_xml).collect();
let assocs: Vec<String> = rt
.associations
.iter()
.map(|a| {
let mut s = format!(
"{}{}<main>{}</main><associationState><state>associated</state></associationState>",
ec2_elem("routeTableAssociationId", &a.association_id),
ec2_elem("routeTableId", &a.route_table_id),
a.main,
);
if let Some(sn) = &a.subnet_id {
s.push_str(&ec2_elem("subnetId", sn));
}
if let Some(gw) = &a.gateway_id {
s.push_str(&ec2_elem("gatewayId", gw));
}
s
})
.collect();
format!(
"{}{}{}{}{}{}",
ec2_elem("routeTableId", &rt.route_table_id),
ec2_elem("vpcId", &rt.vpc_id),
ec2_elem("ownerId", owner),
ec2_list("routeSet", &routes),
ec2_list("associationSet", &assocs),
super::tags::tag_set_xml(tags),
)
}
fn igw_xml(igw: &InternetGateway, tags: &[Tag], owner: &str) -> String {
let atts: Vec<String> = igw
.attachments
.iter()
.map(|(vpc, state)| format!("{}{}", ec2_elem("vpcId", vpc), ec2_elem("state", state)))
.collect();
format!(
"{}{}{}{}",
ec2_elem("internetGatewayId", &igw.internet_gateway_id),
ec2_elem("ownerId", owner),
ec2_list("attachmentSet", &atts),
super::tags::tag_set_xml(tags),
)
}
fn eigw_xml(igw: &InternetGateway, tags: &[Tag]) -> String {
let atts: Vec<String> = igw
.attachments
.iter()
.map(|(vpc, state)| format!("{}{}", ec2_elem("vpcId", vpc), ec2_elem("state", state)))
.collect();
format!(
"{}{}{}",
ec2_elem("egressOnlyInternetGatewayId", &igw.internet_gateway_id),
ec2_list("attachmentSet", &atts),
super::tags::tag_set_xml(tags),
)
}
fn nat_address_xml(allocation_id: &Option<String>) -> String {
let mut s = format!(
"{}<isPrimary>true</isPrimary>{}{}",
ec2_elem("privateIp", "10.0.0.10"),
ec2_elem("networkInterfaceId", "eni-00000000000000000"),
ec2_elem("status", "succeeded"),
);
if let Some(a) = allocation_id {
s.push_str(&ec2_elem("allocationId", a));
s.push_str(&ec2_elem("publicIp", "52.0.0.10"));
s.push_str(&ec2_elem("associationId", "eipassoc-00000000000000000"));
}
s
}
fn nat_xml(n: &NatGateway, tags: &[Tag]) -> String {
let addrs = vec![nat_address_xml(&n.allocation_id)];
format!(
"{}{}{}{}{}{}{}{}",
ec2_elem("natGatewayId", &n.nat_gateway_id),
ec2_elem("subnetId", &n.subnet_id),
ec2_elem("vpcId", &n.vpc_id),
ec2_elem("state", &n.state),
ec2_elem("connectivityType", &n.connectivity_type),
ec2_elem("createTime", FIXED_TIME),
ec2_list("natGatewayAddressSet", &addrs),
super::tags::tag_set_xml(tags),
)
}
fn parse_route(params: &std::collections::HashMap<String, String>) -> Route {
let g = |k: &str| params.get(k).cloned();
Route {
destination_cidr_block: g("DestinationCidrBlock"),
destination_ipv6_cidr_block: g("DestinationIpv6CidrBlock"),
destination_prefix_list_id: g("DestinationPrefixListId"),
gateway_id: g("GatewayId"),
nat_gateway_id: g("NatGatewayId"),
network_interface_id: g("NetworkInterfaceId"),
instance_id: g("InstanceId"),
vpc_peering_connection_id: g("VpcPeeringConnectionId"),
transit_gateway_id: g("TransitGatewayId"),
egress_only_internet_gateway_id: g("EgressOnlyInternetGatewayId"),
state: "active".to_string(),
origin: "CreateRoute".to_string(),
}
}
pub(crate) fn create_route_table(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let vpc_id = require(&req.query_params, "VpcId")?;
let id = gen_id("rtb");
let rt = RouteTable {
route_table_id: id.clone(),
vpc_id,
routes: vec![Route {
destination_cidr_block: Some("10.0.0.0/16".to_string()),
gateway_id: Some("local".to_string()),
state: "active".to_string(),
origin: "CreateRouteTable".to_string(),
..Default::default()
}],
associations: Vec::new(),
};
let owner = req.account_id.clone();
let tags = {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&id,
"route-table",
);
let t = state.tags_for(&id).to_vec();
state.route_tables.insert(id.clone(), rt.clone());
t
};
let body = format!(
"<routeTable>{}</routeTable>{}",
rt_xml(&rt, &tags, &owner),
ec2_elem("clientToken", &gen_id("token")),
);
Ok(Ec2Service::respond(
"CreateRouteTable",
&req.request_id,
&body,
))
}
pub(crate) fn delete_route_table(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "RouteTableId")?;
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.route_tables.remove(&id);
state.tags.remove(&id);
Ok(Ec2Service::respond(
"DeleteRouteTable",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_route_tables(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 100)?;
let filters = parse_filters(&req.query_params);
let wanted = indexed_list(&req.query_params, "RouteTableId");
let owner = req.account_id.clone();
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.route_tables
.values()
.filter(|rt| wanted.is_empty() || wanted.contains(&rt.route_table_id))
.filter(|rt| {
simple_match(
&[
("route-table-id", &rt.route_table_id),
("vpc-id", &rt.vpc_id),
],
state.tags_for(&rt.route_table_id),
&filters,
)
})
.map(|rt| rt_xml(rt, state.tags_for(&rt.route_table_id), &owner))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeRouteTables",
&req.request_id,
&ec2_list("routeTableSet", &items),
))
}
fn route_mutate(
svc: &Ec2Service,
req: &AwsRequest,
action: &str,
replace: bool,
) -> Result<AwsResponse, AwsServiceError> {
let rt_id = require(&req.query_params, "RouteTableId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(rt) = state.route_tables.get_mut(&rt_id) {
let route = parse_route(&req.query_params);
if replace {
if let Some(existing) = rt
.routes
.iter_mut()
.find(|r| r.destination_cidr_block == route.destination_cidr_block)
{
*existing = route;
} else {
rt.routes.push(route);
}
} else {
rt.routes.push(route);
}
}
}
Ok(Ec2Service::respond(
action,
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn create_route(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
route_mutate(svc, req, "CreateRoute", false)
}
pub(crate) fn replace_route(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
route_mutate(svc, req, "ReplaceRoute", true)
}
pub(crate) fn delete_route(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let rt_id = require(&req.query_params, "RouteTableId")?;
let cidr = req.query_params.get("DestinationCidrBlock").cloned();
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(rt) = state.route_tables.get_mut(&rt_id) {
rt.routes
.retain(|r| r.destination_cidr_block != cidr || cidr.is_none());
}
}
Ok(Ec2Service::respond(
"DeleteRoute",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn associate_route_table(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let rt_id = require(&req.query_params, "RouteTableId")?;
let assoc_id = gen_id("rtbassoc");
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(rt) = state.route_tables.get_mut(&rt_id) {
rt.associations.push(RouteTableAssociation {
association_id: assoc_id.clone(),
route_table_id: rt_id.clone(),
subnet_id: req.query_params.get("SubnetId").cloned(),
gateway_id: req.query_params.get("GatewayId").cloned(),
main: false,
});
}
}
let body = format!(
"{}<associationState><state>associated</state></associationState>",
ec2_elem("associationId", &assoc_id)
);
Ok(Ec2Service::respond(
"AssociateRouteTable",
&req.request_id,
&body,
))
}
pub(crate) fn disassociate_route_table(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let assoc_id = require(&req.query_params, "AssociationId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for rt in state.route_tables.values_mut() {
rt.associations.retain(|a| a.association_id != assoc_id);
}
}
Ok(Ec2Service::respond(
"DisassociateRouteTable",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn replace_route_table_association(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let assoc_id = require(&req.query_params, "AssociationId")?;
let rt_id = require(&req.query_params, "RouteTableId")?;
let new_id = gen_id("rtbassoc");
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
for rt in state.route_tables.values_mut() {
for a in rt.associations.iter_mut() {
if a.association_id == assoc_id {
a.association_id = new_id.clone();
a.route_table_id = rt_id.clone();
}
}
}
}
let body = format!(
"{}<associationState><state>associated</state></associationState>",
ec2_elem("newAssociationId", &new_id)
);
Ok(Ec2Service::respond(
"ReplaceRouteTableAssociation",
&req.request_id,
&body,
))
}
pub(crate) fn create_internet_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = gen_id("igw");
let igw = InternetGateway {
internet_gateway_id: id.clone(),
attachments: Vec::new(),
};
let owner = req.account_id.clone();
let tags = {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&id,
"internet-gateway",
);
let t = state.tags_for(&id).to_vec();
state.internet_gateways.insert(id.clone(), igw.clone());
t
};
let body = format!(
"<internetGateway>{}</internetGateway>",
igw_xml(&igw, &tags, &owner)
);
Ok(Ec2Service::respond(
"CreateInternetGateway",
&req.request_id,
&body,
))
}
pub(crate) fn delete_internet_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "InternetGatewayId")?;
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.internet_gateways.remove(&id);
state.tags.remove(&id);
Ok(Ec2Service::respond(
"DeleteInternetGateway",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn describe_internet_gateways(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 1000)?;
let wanted = indexed_list(&req.query_params, "InternetGatewayId");
let owner = req.account_id.clone();
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.internet_gateways
.values()
.filter(|g| wanted.is_empty() || wanted.contains(&g.internet_gateway_id))
.map(|g| igw_xml(g, state.tags_for(&g.internet_gateway_id), &owner))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeInternetGateways",
&req.request_id,
&ec2_list("internetGatewaySet", &items),
))
}
pub(crate) fn attach_internet_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "InternetGatewayId")?;
let vpc = require(&req.query_params, "VpcId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(g) = state.internet_gateways.get_mut(&id) {
g.attachments.push((vpc, "available".to_string()));
}
}
Ok(Ec2Service::respond(
"AttachInternetGateway",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn detach_internet_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "InternetGatewayId")?;
let vpc = require(&req.query_params, "VpcId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
if let Some(g) = state.internet_gateways.get_mut(&id) {
g.attachments.retain(|(v, _)| v != &vpc);
}
}
Ok(Ec2Service::respond(
"DetachInternetGateway",
&req.request_id,
&ec2_return(true),
))
}
pub(crate) fn create_egress_only_igw(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let vpc = require(&req.query_params, "VpcId")?;
let id = gen_id("eigw");
let igw = InternetGateway {
internet_gateway_id: id.clone(),
attachments: vec![(vpc, "attached".to_string())],
};
let tags = {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(
state,
&req.query_params,
&id,
"egress-only-internet-gateway",
);
let t = state.tags_for(&id).to_vec();
state.egress_only_igws.insert(id.clone(), igw.clone());
t
};
let body = format!(
"<egressOnlyInternetGateway>{}</egressOnlyInternetGateway>{}",
eigw_xml(&igw, &tags),
ec2_elem("clientToken", &gen_id("token")),
);
Ok(Ec2Service::respond(
"CreateEgressOnlyInternetGateway",
&req.request_id,
&body,
))
}
pub(crate) fn delete_egress_only_igw(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "EgressOnlyInternetGatewayId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.egress_only_igws.remove(&id);
state.tags.remove(&id);
}
Ok(Ec2Service::respond(
"DeleteEgressOnlyInternetGateway",
&req.request_id,
"<returnCode>true</returnCode>",
))
}
pub(crate) fn describe_egress_only_igws(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 255)?;
let wanted = indexed_list(&req.query_params, "EgressOnlyInternetGatewayId");
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.egress_only_igws
.values()
.filter(|g| wanted.is_empty() || wanted.contains(&g.internet_gateway_id))
.map(|g| eigw_xml(g, state.tags_for(&g.internet_gateway_id)))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeEgressOnlyInternetGateways",
&req.request_id,
&ec2_list("egressOnlyInternetGatewaySet", &items),
))
}
pub(crate) fn create_nat_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_enum(
&req.query_params,
"ConnectivityType",
&["private", "public"],
)?;
validate_enum(
&req.query_params,
"AvailabilityMode",
&["zonal", "regional"],
)?;
validate_int_range(&req.query_params, "SecondaryPrivateIpAddressCount", 1, 31)?;
let id = gen_id("nat");
let nat = NatGateway {
nat_gateway_id: id.clone(),
subnet_id: req
.query_params
.get("SubnetId")
.cloned()
.unwrap_or_default(),
vpc_id: String::new(),
state: "available".to_string(),
connectivity_type: req
.query_params
.get("ConnectivityType")
.cloned()
.unwrap_or_else(|| "public".to_string()),
allocation_id: req.query_params.get("AllocationId").cloned(),
};
let tags = {
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
crate::service::tags::apply_tag_specifications(state, &req.query_params, &id, "natgateway");
let t = state.tags_for(&id).to_vec();
state.nat_gateways.insert(id.clone(), nat.clone());
t
};
let body = format!(
"<natGateway>{}</natGateway>{}",
nat_xml(&nat, &tags),
ec2_elem("clientToken", &gen_id("token")),
);
Ok(Ec2Service::respond(
"CreateNatGateway",
&req.request_id,
&body,
))
}
pub(crate) fn delete_nat_gateway(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "NatGatewayId")?;
{
let mut accounts = svc.state.write();
let state = accounts.get_or_create(&req.account_id);
state.nat_gateways.remove(&id);
state.tags.remove(&id);
}
Ok(Ec2Service::respond(
"DeleteNatGateway",
&req.request_id,
&ec2_elem("natGatewayId", &id),
))
}
pub(crate) fn describe_nat_gateways(
svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
validate_max_results(&req.query_params, 5, 1000)?;
let wanted = indexed_list(&req.query_params, "NatGatewayId");
let accounts = svc.state.read();
let empty = Ec2State::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let mut items: Vec<String> = state
.nat_gateways
.values()
.filter(|n| wanted.is_empty() || wanted.contains(&n.nat_gateway_id))
.map(|n| nat_xml(n, state.tags_for(&n.nat_gateway_id)))
.collect();
items.sort();
Ok(Ec2Service::respond(
"DescribeNatGateways",
&req.request_id,
&ec2_list("natGatewaySet", &items),
))
}
fn nat_address_op(req: &AwsRequest, action: &str) -> Result<AwsResponse, AwsServiceError> {
let id = require(&req.query_params, "NatGatewayId")?;
validate_int_range(&req.query_params, "PrivateIpAddressCount", 1, 31)?;
validate_int_range(&req.query_params, "MaxDrainDurationSeconds", 1, 4000)?;
let body = format!(
"{}{}",
ec2_elem("natGatewayId", &id),
ec2_list(
"natGatewayAddressSet",
&[nat_address_xml(&Some("eipalloc-0".to_string()))]
),
);
Ok(Ec2Service::respond(action, &req.request_id, &body))
}
pub(crate) fn assign_private_nat_gateway_address(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
nat_address_op(req, "AssignPrivateNatGatewayAddress")
}
pub(crate) fn associate_nat_gateway_address(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
nat_address_op(req, "AssociateNatGatewayAddress")
}
pub(crate) fn disassociate_nat_gateway_address(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
nat_address_op(req, "DisassociateNatGatewayAddress")
}
pub(crate) fn unassign_private_nat_gateway_address(
_svc: &Ec2Service,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
nat_address_op(req, "UnassignPrivateNatGatewayAddress")
}
fn simple_match(fields: &[(&str, &str)], tags: &[Tag], filters: &[Filter]) -> bool {
filters.iter().all(|f| {
let candidates: Vec<String> =
if let Some((_, v)) = fields.iter().find(|(n, _)| *n == f.name) {
vec![v.to_string()]
} else if f.name == "tag-key" {
tags.iter().map(|t| t.key.clone()).collect()
} else if let Some(key) = f.name.strip_prefix("tag:") {
tags.iter()
.filter(|t| t.key == key)
.map(|t| t.value.clone())
.collect()
} else {
return true;
};
f.values.iter().any(|v| candidates.iter().any(|c| c == v))
})
}