Skip to main content

awsim_ec2/
lib.rs

1mod error;
2mod ids;
3mod operations;
4mod state;
5
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use awsim_core::{AccountRegionStore, AwsError, Protocol, RequestContext, ServiceHandler};
10use serde_json::Value;
11use tracing::debug;
12
13use state::Ec2State;
14
15/// The AWSim EC2 service handler (networking primitives subset).
16pub struct Ec2Service {
17    store: AccountRegionStore<Ec2State>,
18}
19
20impl Ec2Service {
21    pub fn new() -> Self {
22        Self {
23            store: AccountRegionStore::new(),
24        }
25    }
26
27    fn get_state(&self, ctx: &RequestContext) -> Arc<Ec2State> {
28        self.store.get(&ctx.account_id, &ctx.region)
29    }
30
31    /// Count running instances for a given account+region pair —
32    /// used by the billing meter to charge instance-hours. Stopped /
33    /// terminated / pending instances are excluded since AWS doesn't
34    /// bill compute time for them.
35    pub fn running_instance_count(&self, account_id: &str, region: &str) -> u64 {
36        let state = self.store.get(account_id, region);
37        state
38            .instances
39            .iter()
40            .filter(|i| i.value().state == "running")
41            .count() as u64
42    }
43}
44
45impl Default for Ec2Service {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51#[async_trait]
52impl ServiceHandler for Ec2Service {
53    fn service_name(&self) -> &str {
54        "ec2"
55    }
56
57    fn signing_name(&self) -> &str {
58        "ec2"
59    }
60
61    fn protocol(&self) -> Protocol {
62        Protocol::AwsQuery
63    }
64
65    async fn handle(
66        &self,
67        operation: &str,
68        input: Value,
69        ctx: &RequestContext,
70    ) -> Result<Value, AwsError> {
71        debug!(operation, "EC2 request");
72        let state = self.get_state(ctx);
73
74        // AWS EC2 implements an opt-in dry-run: any mutating operation
75        // that receives DryRun=true must short-circuit with HTTP 412
76        // (DryRunOperation) before the state mutates. Read-only Describe*
77        // operations ignore the flag.
78        if !operation.starts_with("Describe")
79            && input.get("DryRun").and_then(Value::as_bool) == Some(true)
80        {
81            return Err(AwsError::precondition_failed(
82                "DryRunOperation",
83                format!(
84                    "Request would have succeeded, but DryRun flag is set. \
85                     Operation: {operation}."
86                ),
87            ));
88        }
89
90        match operation {
91            // VPCs
92            "CreateVpc" => operations::vpcs::create_vpc(&state, &input),
93            "DeleteVpc" => operations::vpcs::delete_vpc(&state, &input),
94            "DescribeVpcs" => operations::vpcs::describe_vpcs(&state, &input),
95
96            // Subnets
97            "CreateSubnet" => operations::subnets::create_subnet(&state, &input),
98            "DeleteSubnet" => operations::subnets::delete_subnet(&state, &input),
99            "DescribeSubnets" => operations::subnets::describe_subnets(&state, &input),
100
101            // Security Groups
102            "CreateSecurityGroup" => {
103                operations::security_groups::create_security_group(&state, &input)
104            }
105            "DeleteSecurityGroup" => {
106                operations::security_groups::delete_security_group(&state, &input)
107            }
108            "DescribeSecurityGroups" => {
109                operations::security_groups::describe_security_groups(&state, &input)
110            }
111            "AuthorizeSecurityGroupIngress" => {
112                operations::security_groups::authorize_security_group_ingress(&state, &input)
113            }
114            "AuthorizeSecurityGroupEgress" => {
115                operations::security_groups::authorize_security_group_egress(&state, &input)
116            }
117            "RevokeSecurityGroupIngress" => {
118                operations::security_groups::revoke_security_group_ingress(&state, &input)
119            }
120            "RevokeSecurityGroupEgress" => {
121                operations::security_groups::revoke_security_group_egress(&state, &input)
122            }
123
124            // Internet Gateways
125            "CreateInternetGateway" => {
126                operations::gateways::create_internet_gateway(&state, &input)
127            }
128            "DeleteInternetGateway" => {
129                operations::gateways::delete_internet_gateway(&state, &input)
130            }
131            "AttachInternetGateway" => {
132                operations::gateways::attach_internet_gateway(&state, &input)
133            }
134            "DetachInternetGateway" => {
135                operations::gateways::detach_internet_gateway(&state, &input)
136            }
137            "DescribeInternetGateways" => {
138                operations::gateways::describe_internet_gateways(&state, &input)
139            }
140
141            // Route Tables
142            "CreateRouteTable" => operations::route_tables::create_route_table(&state, &input),
143            "DeleteRouteTable" => operations::route_tables::delete_route_table(&state, &input),
144            "DescribeRouteTables" => {
145                operations::route_tables::describe_route_tables(&state, &input)
146            }
147            "CreateRoute" => operations::route_tables::create_route(&state, &input),
148            "AssociateRouteTable" => {
149                operations::route_tables::associate_route_table(&state, &input)
150            }
151
152            // Key Pairs
153            "CreateKeyPair" => operations::key_pairs::create_key_pair(&state, &input),
154            "DeleteKeyPair" => operations::key_pairs::delete_key_pair(&state, &input),
155            "DescribeKeyPairs" => operations::key_pairs::describe_key_pairs(&state, &input),
156
157            // Metadata
158            "DescribeRegions" => operations::metadata::describe_regions(ctx),
159            "DescribeAvailabilityZones" => operations::metadata::describe_availability_zones(ctx),
160
161            // Instances
162            "RunInstances" => operations::instances::run_instances(&state, &input),
163            "DescribeInstances" => operations::instances::describe_instances(&state, &input),
164            "DescribeInstanceAttribute" => {
165                operations::instances::describe_instance_attribute(&state, &input)
166            }
167            "StartInstances" => operations::instances::start_instances(&state, &input),
168            "StopInstances" => operations::instances::stop_instances(&state, &input),
169            "RebootInstances" => operations::instances::reboot_instances(&state, &input),
170            "TerminateInstances" => operations::instances::terminate_instances(&state, &input),
171            "DescribeInstanceStatus" => {
172                operations::instances::describe_instance_status(&state, &input)
173            }
174            "DescribeImages" => operations::instances::describe_images(&state, &input),
175
176            // Tags
177            "CreateTags" => operations::tags::create_tags(&state, &input),
178            "DeleteTags" => operations::tags::delete_tags(&state, &input),
179            "DescribeTags" => operations::tags::describe_tags(&state, &input),
180
181            // Stubs (empty-list responses)
182            "DescribeNetworkInterfaces" => {
183                operations::stubs::describe_network_interfaces(&state, &input)
184            }
185            "DescribeNatGateways" => operations::stubs::describe_nat_gateways(&state, &input),
186            "DescribeVpcEndpoints" => operations::stubs::describe_vpc_endpoints(&state, &input),
187            "DescribeAddresses" => operations::stubs::describe_addresses(&state, &input),
188
189            _ => Err(AwsError::unknown_operation(operation)),
190        }
191    }
192}
193
194#[cfg(test)]
195mod dry_run_tests {
196    use super::*;
197    use awsim_core::ServiceHandler;
198    use serde_json::json;
199
200    fn ctx() -> RequestContext {
201        RequestContext::new("ec2", "us-east-1")
202    }
203
204    fn block_on<F: std::future::Future>(f: F) -> F::Output {
205        use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
206        fn noop_clone(_: *const ()) -> RawWaker {
207            noop_raw_waker()
208        }
209        fn noop(_: *const ()) {}
210        fn noop_raw_waker() -> RawWaker {
211            static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
212            RawWaker::new(std::ptr::null(), &VTABLE)
213        }
214        let waker = unsafe { Waker::from_raw(noop_raw_waker()) };
215        let mut cx = Context::from_waker(&waker);
216        let mut fut = std::pin::pin!(f);
217        loop {
218            match fut.as_mut().poll(&mut cx) {
219                Poll::Ready(v) => return v,
220                Poll::Pending => {}
221            }
222        }
223    }
224
225    #[test]
226    fn write_op_with_dry_run_short_circuits_with_dryrunoperation() {
227        let svc = Ec2Service::new();
228        let err = block_on(svc.handle(
229            "RunInstances",
230            json!({ "ImageId": "ami-12345678", "MinCount": 1, "MaxCount": 1, "DryRun": true }),
231            &ctx(),
232        ))
233        .unwrap_err();
234        assert_eq!(err.code, "DryRunOperation");
235        assert_eq!(err.status.as_u16(), 412);
236    }
237
238    #[test]
239    fn read_op_ignores_dry_run() {
240        let svc = Ec2Service::new();
241        block_on(svc.handle("DescribeInstances", json!({ "DryRun": true }), &ctx())).unwrap();
242    }
243
244    #[test]
245    fn write_op_without_dry_run_still_runs() {
246        let svc = Ec2Service::new();
247        let resp = block_on(svc.handle(
248            "RunInstances",
249            json!({ "ImageId": "ami-12345678", "MinCount": 1, "MaxCount": 1 }),
250            &ctx(),
251        ))
252        .unwrap();
253        assert!(resp.get("instancesSet").is_some() || resp.get("InstancesSet").is_some());
254    }
255}