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
15pub 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 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 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 "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 "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 "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 "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 "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 "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 "DescribeRegions" => operations::metadata::describe_regions(ctx),
159 "DescribeAvailabilityZones" => operations::metadata::describe_availability_zones(ctx),
160
161 "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 "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 "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}