Skip to main content

fakecloud_ec2/
state.rs

1//! EC2 service state.
2//!
3//! Partitioned per account+region via [`fakecloud_core::multi_account`]. The
4//! `tags` map is keyed by EC2 resource id (e.g. `vpc-…`, `i-…`, `sg-…`) and is
5//! the backing store for `CreateTags`/`DeleteTags`/`DescribeTags` plus the
6//! `tag:`/`tag-key` describe filters shared across every resource family.
7
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13
14/// Shared, account-partitioned EC2 state handle.
15pub type SharedEc2State = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<Ec2State>>>;
16
17impl fakecloud_core::multi_account::AccountState for Ec2State {
18    fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
19        Self::new(account_id, region)
20    }
21}
22
23/// A single EC2 resource tag.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Tag {
26    pub key: String,
27    pub value: String,
28}
29
30/// A secondary CIDR-block association on a VPC.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct VpcCidrAssoc {
33    pub association_id: String,
34    pub cidr_block: String,
35    /// `associated` | `disassociated`.
36    pub state: String,
37}
38
39/// A Virtual Private Cloud.
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct Vpc {
42    pub vpc_id: String,
43    pub cidr_block: String,
44    /// `pending` | `available`.
45    pub state: String,
46    pub dhcp_options_id: String,
47    /// `default` | `dedicated` | `host`.
48    pub instance_tenancy: String,
49    pub is_default: bool,
50    pub enable_dns_support: bool,
51    pub enable_dns_hostnames: bool,
52    #[serde(default)]
53    pub cidr_associations: Vec<VpcCidrAssoc>,
54}
55
56/// One `key -> values` entry in a DHCP options set.
57#[derive(Clone, Debug, Serialize, Deserialize)]
58pub struct DhcpConfig {
59    pub key: String,
60    pub values: Vec<String>,
61}
62
63/// A DHCP options set.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct DhcpOptions {
66    pub dhcp_options_id: String,
67    pub configurations: Vec<DhcpConfig>,
68}
69
70/// A subnet within a VPC.
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct Subnet {
73    pub subnet_id: String,
74    pub vpc_id: String,
75    pub cidr_block: String,
76    pub availability_zone: String,
77    pub availability_zone_id: String,
78    /// `pending` | `available`.
79    pub state: String,
80    pub available_ip_address_count: i32,
81    pub default_for_az: bool,
82    pub map_public_ip_on_launch: bool,
83    pub assign_ipv6_address_on_creation: bool,
84    pub map_customer_owned_ip_on_launch: bool,
85    pub enable_dns64: bool,
86    /// `ip-name` | `resource-name`.
87    pub private_dns_hostname_type: String,
88}
89
90/// A CIDR reservation within a subnet.
91#[derive(Clone, Debug, Serialize, Deserialize)]
92pub struct SubnetCidrReservation {
93    pub subnet_cidr_reservation_id: String,
94    pub subnet_id: String,
95    pub cidr: String,
96    /// `prefix` | `explicit`.
97    pub reservation_type: String,
98    pub description: String,
99}
100
101/// A security-group rule (ingress or egress), stored flat.
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct SecurityGroupRule {
104    pub rule_id: String,
105    pub group_id: String,
106    pub is_egress: bool,
107    pub ip_protocol: String,
108    pub from_port: i64,
109    pub to_port: i64,
110    pub cidr_ipv4: Option<String>,
111    pub cidr_ipv6: Option<String>,
112    pub prefix_list_id: Option<String>,
113    pub referenced_group_id: Option<String>,
114    pub description: String,
115}
116
117/// A security group.
118#[derive(Clone, Debug, Serialize, Deserialize)]
119pub struct SecurityGroup {
120    pub group_id: String,
121    pub group_name: String,
122    pub description: String,
123    pub vpc_id: String,
124    #[serde(default)]
125    pub rules: Vec<SecurityGroupRule>,
126}
127
128/// A route within a route table.
129#[derive(Clone, Debug, Default, Serialize, Deserialize)]
130pub struct Route {
131    pub destination_cidr_block: Option<String>,
132    pub destination_ipv6_cidr_block: Option<String>,
133    pub destination_prefix_list_id: Option<String>,
134    pub gateway_id: Option<String>,
135    pub nat_gateway_id: Option<String>,
136    pub network_interface_id: Option<String>,
137    pub instance_id: Option<String>,
138    pub vpc_peering_connection_id: Option<String>,
139    pub transit_gateway_id: Option<String>,
140    pub egress_only_internet_gateway_id: Option<String>,
141    /// `active` | `blackhole`.
142    pub state: String,
143    /// `CreateRouteTable` | `CreateRoute`.
144    pub origin: String,
145}
146
147/// A route-table association (to a subnet or gateway, or the VPC main table).
148#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct RouteTableAssociation {
150    pub association_id: String,
151    pub route_table_id: String,
152    pub subnet_id: Option<String>,
153    pub gateway_id: Option<String>,
154    pub main: bool,
155}
156
157/// A route table.
158#[derive(Clone, Debug, Serialize, Deserialize)]
159pub struct RouteTable {
160    pub route_table_id: String,
161    pub vpc_id: String,
162    #[serde(default)]
163    pub routes: Vec<Route>,
164    #[serde(default)]
165    pub associations: Vec<RouteTableAssociation>,
166}
167
168/// An internet gateway (or egress-only IGW) with its VPC attachments.
169#[derive(Clone, Debug, Serialize, Deserialize)]
170pub struct InternetGateway {
171    pub internet_gateway_id: String,
172    /// (vpc_id, state) pairs.
173    #[serde(default)]
174    pub attachments: Vec<(String, String)>,
175}
176
177/// A NAT gateway.
178#[derive(Clone, Debug, Serialize, Deserialize)]
179pub struct NatGateway {
180    pub nat_gateway_id: String,
181    pub subnet_id: String,
182    pub vpc_id: String,
183    /// `pending` | `available` | `deleting` | `deleted`.
184    pub state: String,
185    /// `public` | `private`.
186    pub connectivity_type: String,
187    pub allocation_id: Option<String>,
188}
189
190/// An Elastic IP allocation.
191#[derive(Clone, Debug, Serialize, Deserialize)]
192pub struct ElasticIp {
193    pub allocation_id: String,
194    pub public_ip: String,
195    /// `vpc` | `standard`.
196    pub domain: String,
197    pub association_id: Option<String>,
198    pub instance_id: Option<String>,
199    pub network_interface_id: Option<String>,
200    pub private_ip_address: Option<String>,
201}
202
203/// An EC2 key pair (public-key metadata only).
204#[derive(Clone, Debug, Serialize, Deserialize)]
205pub struct KeyPair {
206    pub key_pair_id: String,
207    pub key_name: String,
208    /// `rsa` | `ed25519`.
209    pub key_type: String,
210    pub key_fingerprint: String,
211}
212
213/// A placement group.
214#[derive(Clone, Debug, Serialize, Deserialize)]
215pub struct PlacementGroup {
216    pub group_id: String,
217    pub group_name: String,
218    /// `cluster` | `spread` | `partition`.
219    pub strategy: String,
220    /// `available`.
221    pub state: String,
222    pub partition_count: Option<i64>,
223    pub spread_level: Option<String>,
224}
225
226/// An ENI attachment.
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct EniAttachment {
229    pub attachment_id: String,
230    pub instance_id: String,
231    pub device_index: i64,
232    /// `attaching` | `attached` | `detaching` | `detached`.
233    pub status: String,
234}
235
236/// An elastic network interface.
237#[derive(Clone, Debug, Serialize, Deserialize)]
238pub struct NetworkInterface {
239    pub network_interface_id: String,
240    pub subnet_id: String,
241    pub vpc_id: String,
242    pub availability_zone: String,
243    pub description: String,
244    pub mac_address: String,
245    pub private_ip_address: String,
246    /// `available` | `in-use`.
247    pub status: String,
248    pub interface_type: String,
249    pub source_dest_check: bool,
250    #[serde(default)]
251    pub group_ids: Vec<String>,
252    #[serde(default)]
253    pub private_ips: Vec<String>,
254    #[serde(default)]
255    pub ipv6_addresses: Vec<String>,
256    pub attachment: Option<EniAttachment>,
257}
258
259/// A network-interface permission grant.
260#[derive(Clone, Debug, Serialize, Deserialize)]
261pub struct NetworkInterfacePermission {
262    pub permission_id: String,
263    pub network_interface_id: String,
264    pub aws_account_id: String,
265    /// `INSTANCE-ATTACH` | `EIP-ASSOCIATE`.
266    pub permission: String,
267}
268
269/// An EC2 instance (metadata-faithful; a Docker-backed runtime layers on top).
270#[derive(Clone, Debug, Serialize, Deserialize)]
271pub struct Instance {
272    pub instance_id: String,
273    pub image_id: String,
274    pub instance_type: String,
275    /// EC2 state code: 0 pending, 16 running, 32 shutting-down, 48 terminated,
276    /// 64 stopping, 80 stopped.
277    pub state_code: i64,
278    pub state_name: String,
279    pub private_ip: String,
280    pub public_ip: Option<String>,
281    pub subnet_id: Option<String>,
282    pub vpc_id: Option<String>,
283    pub key_name: Option<String>,
284    #[serde(default)]
285    pub security_group_ids: Vec<String>,
286    pub reservation_id: String,
287    pub ami_launch_index: i64,
288    pub monitoring: bool,
289    pub az: String,
290    pub launch_time: String,
291    /// Id of the backing container/Pod, when this instance is backed by a
292    /// real container runtime. `None` in metadata-only mode.
293    #[serde(default)]
294    pub container_id: Option<String>,
295    // ---- modifiable instance attributes (ModifyInstanceAttribute et al.) ----
296    /// `disableApiTermination` — when true, TerminateInstances is rejected.
297    #[serde(default)]
298    pub disable_api_termination: bool,
299    /// `disableApiStop` — when true, StopInstances is rejected.
300    #[serde(default)]
301    pub disable_api_stop: bool,
302    /// `sourceDestCheck` — defaults to true on AWS.
303    #[serde(default = "default_true")]
304    pub source_dest_check: bool,
305    /// `ebsOptimized`.
306    #[serde(default)]
307    pub ebs_optimized: bool,
308    /// `instanceInitiatedShutdownBehavior` — `stop` (default) | `terminate`.
309    #[serde(default = "default_shutdown_behavior")]
310    pub instance_initiated_shutdown_behavior: String,
311    /// `userData` — base64-encoded, as supplied at launch / via Modify.
312    #[serde(default)]
313    pub user_data: Option<String>,
314    // ---- Modify*Options round-trip state ----
315    /// `metadataOptions` (`ModifyInstanceMetadataOptions`).
316    #[serde(default)]
317    pub metadata_options: MetadataOptions,
318    /// `cpuOptions` (`ModifyInstanceCpuOptions`).
319    #[serde(default)]
320    pub cpu_options: Option<CpuOptions>,
321    /// `bandwidthWeighting` (`ModifyInstanceNetworkPerformanceOptions`).
322    #[serde(default)]
323    pub bandwidth_weighting: Option<String>,
324    /// `maintenanceOptions` (`ModifyInstanceMaintenanceOptions`).
325    #[serde(default)]
326    pub maintenance_options: MaintenanceOptions,
327    /// `placement` tenancy/affinity overrides (`ModifyInstancePlacement`).
328    #[serde(default)]
329    pub placement_tenancy: Option<String>,
330    #[serde(default)]
331    pub placement_affinity: Option<String>,
332    #[serde(default)]
333    pub placement_group_name: Option<String>,
334}
335
336fn default_true() -> bool {
337    true
338}
339
340fn default_shutdown_behavior() -> String {
341    "stop".to_string()
342}
343
344/// IMDS (instance-metadata service) options, round-tripped by
345/// `ModifyInstanceMetadataOptions` and reflected in DescribeInstances.
346#[derive(Clone, Debug, Serialize, Deserialize)]
347pub struct MetadataOptions {
348    /// `optional` | `required`.
349    pub http_tokens: String,
350    /// `disabled` | `enabled`.
351    pub http_endpoint: String,
352    pub http_put_response_hop_limit: i64,
353    /// `disabled` | `enabled`.
354    pub http_protocol_ipv6: String,
355    /// `disabled` | `enabled`.
356    pub instance_metadata_tags: String,
357}
358
359impl Default for MetadataOptions {
360    fn default() -> Self {
361        Self {
362            http_tokens: "optional".to_string(),
363            http_endpoint: "enabled".to_string(),
364            http_put_response_hop_limit: 1,
365            http_protocol_ipv6: "disabled".to_string(),
366            instance_metadata_tags: "disabled".to_string(),
367        }
368    }
369}
370
371/// CPU options round-tripped by `ModifyInstanceCpuOptions`.
372#[derive(Clone, Debug, Serialize, Deserialize)]
373pub struct CpuOptions {
374    pub core_count: i64,
375    pub threads_per_core: i64,
376}
377
378/// Maintenance options round-tripped by `ModifyInstanceMaintenanceOptions`.
379#[derive(Clone, Debug, Serialize, Deserialize)]
380pub struct MaintenanceOptions {
381    /// `disabled` | `default`.
382    pub auto_recovery: String,
383    /// `disabled` | `default`.
384    pub reboot_migration: String,
385}
386
387impl Default for MaintenanceOptions {
388    fn default() -> Self {
389        Self {
390            auto_recovery: "default".to_string(),
391            reboot_migration: "default".to_string(),
392        }
393    }
394}
395
396/// An EBS volume attachment.
397#[derive(Clone, Debug, Serialize, Deserialize)]
398pub struct VolumeAttachment {
399    pub volume_id: String,
400    pub instance_id: String,
401    pub device: String,
402    /// `attaching` | `attached` | `detaching` | `detached`.
403    pub status: String,
404    pub delete_on_termination: bool,
405}
406
407/// An EBS volume.
408#[derive(Clone, Debug, Serialize, Deserialize)]
409pub struct Volume {
410    pub volume_id: String,
411    pub size: i64,
412    pub snapshot_id: Option<String>,
413    pub availability_zone: String,
414    /// `creating` | `available` | `in-use` | `deleting` | `deleted`.
415    pub state: String,
416    pub volume_type: String,
417    pub iops: Option<i64>,
418    pub throughput: Option<i64>,
419    pub encrypted: bool,
420    pub kms_key_id: Option<String>,
421    pub multi_attach_enabled: bool,
422    pub auto_enable_io: bool,
423    #[serde(default)]
424    pub attachments: Vec<VolumeAttachment>,
425    #[serde(default)]
426    pub in_recycle_bin: bool,
427}
428
429/// An EBS snapshot.
430#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct Snapshot {
432    pub snapshot_id: String,
433    pub volume_id: String,
434    /// `pending` | `completed` | `error`.
435    pub state: String,
436    pub volume_size: i64,
437    pub description: String,
438    pub encrypted: bool,
439    /// `standard` | `archive`.
440    pub storage_tier: String,
441    #[serde(default)]
442    pub in_recycle_bin: bool,
443    #[serde(default)]
444    pub locked: bool,
445    pub lock_mode: Option<String>,
446}
447
448/// An AMI (machine image).
449#[derive(Clone, Debug, Serialize, Deserialize)]
450pub struct Image {
451    pub image_id: String,
452    pub name: String,
453    pub description: String,
454    /// `pending` | `available` | `disabled` | `deregistered`.
455    pub state: String,
456    pub architecture: String,
457    pub public: bool,
458    pub source_instance_id: Option<String>,
459    #[serde(default)]
460    pub in_recycle_bin: bool,
461    pub deprecation_time: Option<String>,
462    #[serde(default)]
463    pub deregistration_protection: bool,
464    /// `launchPermission` — AWS account ids the AMI is explicitly shared with
465    /// (cross-account share via `ModifyImageAttribute`).
466    #[serde(default)]
467    pub launch_permission_users: Vec<String>,
468    /// `launchPermission` groups — only `all` is valid in AWS (public share).
469    #[serde(default)]
470    pub launch_permission_groups: Vec<String>,
471    /// `bootMode` — `legacy-bios` | `uefi` | `uefi-preferred`. `None` reports
472    /// the default `uefi`; settable via `ModifyImageAttribute`.
473    #[serde(default)]
474    pub boot_mode: Option<String>,
475}
476
477/// A network ACL entry (rule).
478#[derive(Clone, Debug, Serialize, Deserialize)]
479pub struct NetworkAclEntry {
480    pub rule_number: i64,
481    pub protocol: String,
482    /// `allow` | `deny`.
483    pub rule_action: String,
484    pub egress: bool,
485    pub cidr_block: Option<String>,
486    pub ipv6_cidr_block: Option<String>,
487    /// TCP/UDP port range (from, to).
488    pub port_range: Option<(i64, i64)>,
489    /// ICMP (type, code).
490    pub icmp_type_code: Option<(i64, i64)>,
491}
492
493/// A network ACL <-> subnet association.
494#[derive(Clone, Debug, Serialize, Deserialize)]
495pub struct NetworkAclAssoc {
496    pub association_id: String,
497    pub subnet_id: String,
498}
499
500/// A network ACL.
501#[derive(Clone, Debug, Serialize, Deserialize)]
502pub struct NetworkAcl {
503    pub network_acl_id: String,
504    pub vpc_id: String,
505    pub is_default: bool,
506    #[serde(default)]
507    pub entries: Vec<NetworkAclEntry>,
508    #[serde(default)]
509    pub associations: Vec<NetworkAclAssoc>,
510}
511
512/// A VPC peering connection.
513#[derive(Clone, Debug, Serialize, Deserialize)]
514pub struct VpcPeering {
515    pub id: String,
516    pub requester_vpc_id: String,
517    pub accepter_vpc_id: String,
518    /// `pending-acceptance` | `active` | `rejected` | `deleted`.
519    pub status: String,
520    /// Requester-side DNS-resolution-from-remote-VPC option.
521    #[serde(default)]
522    pub requester_allow_dns: bool,
523    /// Accepter-side DNS-resolution-from-remote-VPC option.
524    #[serde(default)]
525    pub accepter_allow_dns: bool,
526}
527
528/// A VPC endpoint.
529#[derive(Clone, Debug, Serialize, Deserialize)]
530pub struct VpcEndpoint {
531    pub id: String,
532    /// `Interface` | `Gateway` | `GatewayLoadBalancer` | ...
533    pub endpoint_type: String,
534    pub vpc_id: String,
535    pub service_name: String,
536    pub state: String,
537    #[serde(default)]
538    pub subnet_ids: Vec<String>,
539    #[serde(default)]
540    pub route_table_ids: Vec<String>,
541    #[serde(default)]
542    pub private_dns_enabled: bool,
543}
544
545/// A VPC endpoint service configuration (PrivateLink provider side).
546#[derive(Clone, Debug, Serialize, Deserialize)]
547pub struct EndpointService {
548    pub service_id: String,
549    pub service_name: String,
550    pub state: String,
551    pub acceptance_required: bool,
552    pub payer_responsibility: String,
553    #[serde(default)]
554    pub nlb_arns: Vec<String>,
555}
556
557/// A VPC endpoint connection notification.
558#[derive(Clone, Debug, Serialize, Deserialize)]
559pub struct ConnectionNotification {
560    pub id: String,
561    pub arn: String,
562    pub service_id: Option<String>,
563    #[serde(default)]
564    pub events: Vec<String>,
565}
566
567/// A VPC flow log.
568#[derive(Clone, Debug, Serialize, Deserialize)]
569pub struct FlowLog {
570    pub id: String,
571    pub resource_id: String,
572    pub traffic_type: String,
573    pub log_destination_type: String,
574    pub log_group_name: Option<String>,
575    /// Destination ARN for `s3` / `kinesis-data-firehose` deliveries.
576    pub log_destination: Option<String>,
577}
578
579/// A launch template (versions tracked as monotonic counters).
580#[derive(Clone, Debug, Serialize, Deserialize)]
581pub struct LaunchTemplate {
582    pub id: String,
583    pub name: String,
584    pub default_version: i64,
585    pub latest_version: i64,
586}
587
588/// A Spot instance request.
589#[derive(Clone, Debug, Serialize, Deserialize)]
590pub struct SpotRequest {
591    pub id: String,
592    /// `open` | `active` | `cancelled` | `closed`.
593    pub state: String,
594    pub request_type: String,
595    pub spot_price: String,
596}
597
598/// A Spot fleet request.
599#[derive(Clone, Debug, Serialize, Deserialize)]
600pub struct SpotFleet {
601    pub id: String,
602    pub state: String,
603}
604
605/// An EC2 fleet.
606#[derive(Clone, Debug, Serialize, Deserialize)]
607pub struct Fleet {
608    pub id: String,
609    pub state: String,
610    pub fleet_type: String,
611}
612
613/// An on-demand capacity reservation.
614#[derive(Clone, Debug, Serialize, Deserialize)]
615pub struct CapacityReservation {
616    pub id: String,
617    pub instance_type: String,
618    pub instance_platform: String,
619    pub availability_zone: String,
620    pub tenancy: String,
621    pub total_instance_count: i64,
622    pub available_instance_count: i64,
623    /// `active` | `expired` | `cancelled` | `pending` | `failed`.
624    pub state: String,
625    pub end_date_type: String,
626    pub instance_match_criteria: String,
627}
628
629/// A Reserved Instance purchase.
630#[derive(Clone, Debug, Serialize, Deserialize)]
631pub struct ReservedInstances {
632    pub id: String,
633    pub instance_type: String,
634    pub availability_zone: String,
635    pub instance_count: i64,
636    pub product_description: String,
637    pub state: String,
638    pub duration: i64,
639    pub fixed_price: String,
640    pub usage_price: String,
641}
642
643/// A Reserved Instances listing in the Reserved Instance Marketplace.
644#[derive(Clone, Debug, Serialize, Deserialize)]
645pub struct ReservedInstancesListing {
646    pub listing_id: String,
647    pub reserved_instances_id: String,
648    pub instance_count: i64,
649    pub client_token: String,
650    /// `active` | `cancelled` | `closed`.
651    pub status: String,
652    pub status_message: String,
653}
654
655/// A Reserved Instances modification request.
656#[derive(Clone, Debug, Serialize, Deserialize)]
657pub struct ReservedInstancesModification {
658    pub modification_id: String,
659    pub reserved_instances_ids: Vec<String>,
660    /// `processing` | `fulfilled` | `failed`.
661    pub status: String,
662    pub client_token: String,
663}
664
665/// A Dedicated Host.
666#[derive(Clone, Debug, Serialize, Deserialize)]
667pub struct DedicatedHost {
668    pub id: String,
669    pub auto_placement: String,
670    pub availability_zone: String,
671    pub instance_type: String,
672    pub state: String,
673    pub host_recovery: String,
674    pub host_maintenance: String,
675}
676
677/// A Transit Gateway.
678#[derive(Clone, Debug, Serialize, Deserialize)]
679pub struct TransitGateway {
680    pub id: String,
681    pub description: String,
682    /// `pending` | `available` | `modifying` | `deleting` | `deleted`.
683    #[serde(default = "tgw_default_state")]
684    pub state: String,
685}
686
687fn tgw_default_state() -> String {
688    "available".to_string()
689}
690
691/// A Transit Gateway attachment (VPC and others).
692#[derive(Clone, Debug, Serialize, Deserialize)]
693pub struct TgwAttachment {
694    pub id: String,
695    pub tgw_id: String,
696    pub resource_id: String,
697    pub resource_type: String,
698    #[serde(default)]
699    pub subnet_ids: Vec<String>,
700    pub state: String,
701}
702
703/// A Transit Gateway route table.
704#[derive(Clone, Debug, Serialize, Deserialize)]
705pub struct TgwRouteTable {
706    pub id: String,
707    pub tgw_id: String,
708}
709
710/// A static Transit Gateway route within a route table.
711#[derive(Clone, Debug, Serialize, Deserialize)]
712pub struct TgwRoute {
713    pub cidr: String,
714    pub attachment_id: String,
715    pub state: String,
716}
717
718/// A Transit Gateway multicast domain.
719#[derive(Clone, Debug, Serialize, Deserialize)]
720pub struct TgwMulticastDomain {
721    pub id: String,
722    pub tgw_id: String,
723}
724
725/// A Transit Gateway metering policy.
726#[derive(Clone, Debug, Serialize, Deserialize)]
727pub struct TgwMeteringPolicy {
728    pub id: String,
729    pub tgw_id: String,
730}
731
732/// A customer gateway (on-prem side of a VPN).
733#[derive(Clone, Debug, Serialize, Deserialize)]
734pub struct CustomerGateway {
735    pub id: String,
736    pub state: String,
737    pub ip_address: String,
738    pub bgp_asn: String,
739}
740
741/// A virtual private gateway.
742#[derive(Clone, Debug, Serialize, Deserialize)]
743pub struct VpnGateway {
744    pub id: String,
745    pub state: String,
746    #[serde(default)]
747    pub attachments: Vec<String>,
748}
749
750/// A Site-to-Site VPN connection.
751#[derive(Clone, Debug, Serialize, Deserialize)]
752pub struct VpnConnection {
753    pub id: String,
754    pub state: String,
755    pub customer_gateway_id: String,
756    pub vpn_gateway_id: Option<String>,
757    #[serde(default)]
758    pub routes: Vec<String>,
759}
760
761/// A VPN concentrator.
762#[derive(Clone, Debug, Serialize, Deserialize)]
763pub struct VpnConcentrator {
764    pub id: String,
765    pub state: String,
766}
767
768/// An IPAM (IP Address Manager).
769#[derive(Clone, Debug, Serialize, Deserialize)]
770pub struct Ipam {
771    pub id: String,
772    pub public_scope_id: String,
773    pub private_scope_id: String,
774    pub tier: String,
775    #[serde(default)]
776    pub description: String,
777}
778
779/// An IPAM scope.
780#[derive(Clone, Debug, Serialize, Deserialize)]
781pub struct IpamScope {
782    pub id: String,
783    pub ipam_id: String,
784    /// "public" or "private".
785    #[serde(default)]
786    pub scope_type: String,
787    #[serde(default)]
788    pub description: String,
789}
790
791/// An IPAM pool.
792#[derive(Clone, Debug, Serialize, Deserialize)]
793pub struct IpamPool {
794    pub id: String,
795    pub scope_id: String,
796    pub address_family: String,
797    #[serde(default)]
798    pub description: String,
799}
800
801/// An IPAM resource discovery.
802#[derive(Clone, Debug, Serialize, Deserialize)]
803pub struct IpamResourceDiscovery {
804    pub id: String,
805    #[serde(default)]
806    pub description: String,
807}
808
809/// An IPAM policy.
810#[derive(Clone, Debug, Serialize, Deserialize)]
811pub struct IpamPolicy {
812    pub id: String,
813    pub ipam_id: String,
814}
815
816/// An IPAM prefix-list resolver.
817#[derive(Clone, Debug, Serialize, Deserialize)]
818pub struct IpamPrefixListResolver {
819    pub id: String,
820    pub ipam_id: String,
821    pub address_family: String,
822    #[serde(default)]
823    pub description: String,
824}
825
826/// An IPAM prefix-list resolver target.
827#[derive(Clone, Debug, Serialize, Deserialize)]
828pub struct IpamPrefixListResolverTarget {
829    pub id: String,
830    pub resolver_id: String,
831    pub prefix_list_id: String,
832    pub prefix_list_region: String,
833    #[serde(default)]
834    pub track_latest_version: bool,
835}
836
837/// A Verified Access instance.
838#[derive(Clone, Debug, Serialize, Deserialize)]
839pub struct VerifiedAccessInstance {
840    pub id: String,
841    pub description: String,
842    #[serde(default)]
843    pub trust_providers: Vec<String>,
844}
845
846/// A Verified Access trust provider.
847#[derive(Clone, Debug, Serialize, Deserialize)]
848pub struct VerifiedAccessTrustProvider {
849    pub id: String,
850    pub trust_provider_type: String,
851    pub policy_reference_name: String,
852    pub description: String,
853}
854
855/// A Verified Access group.
856#[derive(Clone, Debug, Serialize, Deserialize)]
857pub struct VerifiedAccessGroup {
858    pub id: String,
859    pub instance_id: String,
860    pub description: String,
861}
862
863/// A Verified Access endpoint.
864#[derive(Clone, Debug, Serialize, Deserialize)]
865pub struct VerifiedAccessEndpoint {
866    pub id: String,
867    pub group_id: String,
868    pub instance_id: String,
869    pub endpoint_type: String,
870    pub attachment_type: String,
871}
872
873/// A Network Insights reachability path.
874#[derive(Clone, Debug, Serialize, Deserialize)]
875pub struct NetworkInsightsPath {
876    pub id: String,
877    pub source: String,
878    pub destination: String,
879    pub protocol: String,
880}
881
882/// A Network Insights path analysis.
883#[derive(Clone, Debug, Serialize, Deserialize)]
884pub struct NetworkInsightsAnalysis {
885    pub id: String,
886    pub path_id: String,
887}
888
889/// A Network Insights access scope.
890#[derive(Clone, Debug, Serialize, Deserialize)]
891pub struct NetworkInsightsAccessScope {
892    pub id: String,
893}
894
895/// A Network Insights access-scope analysis.
896#[derive(Clone, Debug, Serialize, Deserialize)]
897pub struct NetworkInsightsAccessScopeAnalysis {
898    pub id: String,
899    pub scope_id: String,
900}
901
902/// A carrier gateway (Wavelength).
903#[derive(Clone, Debug, Serialize, Deserialize)]
904pub struct CarrierGateway {
905    pub id: String,
906    pub vpc_id: String,
907}
908
909/// An EC2 Instance Connect endpoint.
910#[derive(Clone, Debug, Serialize, Deserialize)]
911pub struct InstanceConnectEndpoint {
912    pub id: String,
913    pub subnet_id: String,
914}
915
916/// A customer-owned IP (CoIP) pool.
917#[derive(Clone, Debug, Serialize, Deserialize)]
918pub struct CoipPool {
919    pub id: String,
920    pub route_table_id: String,
921}
922
923/// A local-gateway route table.
924#[derive(Clone, Debug, Serialize, Deserialize)]
925pub struct LocalGatewayRouteTable {
926    pub id: String,
927    pub local_gateway_id: String,
928    pub mode: String,
929}
930
931/// A local-gateway route-table <-> VPC association.
932#[derive(Clone, Debug, Serialize, Deserialize)]
933pub struct LocalGatewayRouteTableVpcAssoc {
934    pub id: String,
935    pub route_table_id: String,
936    pub vpc_id: String,
937}
938
939/// A local-gateway virtual interface.
940#[derive(Clone, Debug, Serialize, Deserialize)]
941pub struct LocalGatewayVif {
942    pub id: String,
943    pub group_id: String,
944    pub vlan: String,
945    pub local_address: String,
946    pub peer_address: String,
947}
948
949/// A local-gateway virtual-interface group.
950#[derive(Clone, Debug, Serialize, Deserialize)]
951pub struct LocalGatewayVifGroup {
952    pub id: String,
953    pub local_gateway_id: String,
954}
955
956/// A local-gateway route-table <-> virtual-interface-group association.
957#[derive(Clone, Debug, Serialize, Deserialize)]
958pub struct LocalGatewayRouteTableVifgAssoc {
959    pub id: String,
960    pub route_table_id: String,
961    pub vif_group_id: String,
962}
963
964/// A Client VPN endpoint.
965#[derive(Clone, Debug, Serialize, Deserialize)]
966pub struct ClientVpnEndpoint {
967    pub id: String,
968    pub description: String,
969    pub status: String,
970    pub server_cert_arn: String,
971    pub transport_protocol: String,
972    pub client_cidr: String,
973    #[serde(default)]
974    pub routes: Vec<String>,
975    /// (association id, subnet id) for each associated target network.
976    #[serde(default)]
977    pub target_networks: Vec<(String, String)>,
978    /// Ingress authorization rule target CIDRs.
979    #[serde(default)]
980    pub auth_rules: Vec<String>,
981}
982
983/// A Transit Gateway peering attachment.
984#[derive(Clone, Debug, Serialize, Deserialize)]
985pub struct TgwPeering {
986    pub id: String,
987    pub tgw_id: String,
988    pub peer_tgw_id: String,
989    pub peer_account: String,
990    pub peer_region: String,
991    pub state: String,
992}
993
994/// Per-account, per-region EC2 state. Resource families are added to this
995/// struct as their batches land.
996#[derive(Clone, Debug, Default, Serialize, Deserialize)]
997pub struct Ec2State {
998    pub account_id: String,
999    pub region: String,
1000    /// resource-id -> tags. Shared by every Describe* `tag:` filter.
1001    #[serde(default)]
1002    pub tags: BTreeMap<String, Vec<Tag>>,
1003    #[serde(default)]
1004    pub vpcs: BTreeMap<String, Vpc>,
1005    #[serde(default)]
1006    pub dhcp_options: BTreeMap<String, DhcpOptions>,
1007    #[serde(default)]
1008    pub subnets: BTreeMap<String, Subnet>,
1009    #[serde(default)]
1010    pub subnet_cidr_reservations: BTreeMap<String, SubnetCidrReservation>,
1011    #[serde(default)]
1012    pub security_groups: BTreeMap<String, SecurityGroup>,
1013    #[serde(default)]
1014    pub route_tables: BTreeMap<String, RouteTable>,
1015    #[serde(default)]
1016    pub internet_gateways: BTreeMap<String, InternetGateway>,
1017    #[serde(default)]
1018    pub egress_only_igws: BTreeMap<String, InternetGateway>,
1019    #[serde(default)]
1020    pub nat_gateways: BTreeMap<String, NatGateway>,
1021    /// keyed by allocation id.
1022    #[serde(default)]
1023    pub elastic_ips: BTreeMap<String, ElasticIp>,
1024    /// keyed by key name.
1025    #[serde(default)]
1026    pub key_pairs: BTreeMap<String, KeyPair>,
1027    /// keyed by group name.
1028    #[serde(default)]
1029    pub placement_groups: BTreeMap<String, PlacementGroup>,
1030    #[serde(default)]
1031    pub network_interfaces: BTreeMap<String, NetworkInterface>,
1032    /// keyed by permission id.
1033    #[serde(default)]
1034    pub eni_permissions: BTreeMap<String, NetworkInterfacePermission>,
1035    #[serde(default)]
1036    pub instances: BTreeMap<String, Instance>,
1037    #[serde(default)]
1038    pub volumes: BTreeMap<String, Volume>,
1039    /// Account-level EBS default encryption toggle.
1040    #[serde(default)]
1041    pub ebs_encryption_default: bool,
1042    /// Account-level EBS default KMS key (None = `alias/aws/ebs`).
1043    #[serde(default)]
1044    pub ebs_default_kms_key_id: Option<String>,
1045    #[serde(default)]
1046    pub snapshots: BTreeMap<String, Snapshot>,
1047    /// Account-level snapshot block-public-access state.
1048    #[serde(default)]
1049    pub snapshot_block_public_access: String,
1050    #[serde(default)]
1051    pub images: BTreeMap<String, Image>,
1052    /// Watermarks attached to AMIs: image_id -> watermark_key -> watermark_name.
1053    #[serde(default)]
1054    pub image_watermarks: BTreeMap<String, BTreeMap<String, String>>,
1055    /// Account-level image block-public-access state.
1056    #[serde(default)]
1057    pub image_block_public_access: String,
1058    /// Account-level allowed-images settings state.
1059    #[serde(default)]
1060    pub allowed_images_settings: String,
1061    #[serde(default)]
1062    pub network_acls: BTreeMap<String, NetworkAcl>,
1063    #[serde(default)]
1064    pub vpc_peerings: BTreeMap<String, VpcPeering>,
1065    #[serde(default)]
1066    pub vpc_endpoints: BTreeMap<String, VpcEndpoint>,
1067    #[serde(default)]
1068    pub endpoint_services: BTreeMap<String, EndpointService>,
1069    #[serde(default)]
1070    pub connection_notifications: BTreeMap<String, ConnectionNotification>,
1071    #[serde(default)]
1072    pub flow_logs: BTreeMap<String, FlowLog>,
1073    #[serde(default)]
1074    pub launch_templates: BTreeMap<String, LaunchTemplate>,
1075    #[serde(default)]
1076    pub spot_requests: BTreeMap<String, SpotRequest>,
1077    #[serde(default)]
1078    pub spot_fleets: BTreeMap<String, SpotFleet>,
1079    #[serde(default)]
1080    pub fleets: BTreeMap<String, Fleet>,
1081    /// Account-level spot datafeed subscription (bucket, prefix).
1082    #[serde(default)]
1083    pub spot_datafeed: Option<(String, String)>,
1084    #[serde(default)]
1085    pub capacity_reservations: BTreeMap<String, CapacityReservation>,
1086    /// Capacity reservation fleet ids (metadata-only).
1087    #[serde(default)]
1088    pub capacity_reservation_fleets: BTreeMap<String, String>,
1089    #[serde(default)]
1090    pub reserved_instances: BTreeMap<String, ReservedInstances>,
1091    #[serde(default)]
1092    pub reserved_instances_listings: BTreeMap<String, ReservedInstancesListing>,
1093    #[serde(default)]
1094    pub reserved_instances_modifications: BTreeMap<String, ReservedInstancesModification>,
1095    #[serde(default)]
1096    pub dedicated_hosts: BTreeMap<String, DedicatedHost>,
1097    #[serde(default)]
1098    pub transit_gateways: BTreeMap<String, TransitGateway>,
1099    #[serde(default)]
1100    pub tgw_attachments: BTreeMap<String, TgwAttachment>,
1101    #[serde(default)]
1102    pub tgw_route_tables: BTreeMap<String, TgwRouteTable>,
1103    /// route-table-id -> static routes.
1104    #[serde(default)]
1105    pub tgw_routes: BTreeMap<String, Vec<TgwRoute>>,
1106    /// route-table-id -> associated attachment ids.
1107    #[serde(default)]
1108    pub tgw_rt_associations: BTreeMap<String, Vec<String>>,
1109    /// route-table-id -> propagated attachment ids.
1110    #[serde(default)]
1111    pub tgw_rt_propagations: BTreeMap<String, Vec<String>>,
1112    /// route-table-id -> prefix-list ids referenced.
1113    #[serde(default)]
1114    pub tgw_prefix_list_refs: BTreeMap<String, Vec<String>>,
1115    #[serde(default)]
1116    pub tgw_peerings: BTreeMap<String, TgwPeering>,
1117    /// connect-attachment-id -> (transport attachment id, tgw id).
1118    #[serde(default)]
1119    pub tgw_connects: BTreeMap<String, (String, String)>,
1120    /// connect-peer-id -> attachment id.
1121    #[serde(default)]
1122    pub tgw_connect_peers: BTreeMap<String, String>,
1123    /// policy-table-id -> tgw id.
1124    #[serde(default)]
1125    pub tgw_policy_tables: BTreeMap<String, String>,
1126    /// policy-table-id -> associated attachment ids.
1127    #[serde(default)]
1128    pub tgw_policy_table_associations: BTreeMap<String, Vec<String>>,
1129    /// announcement-id -> (route-table id, peering-attachment id).
1130    #[serde(default)]
1131    pub tgw_announcements: BTreeMap<String, (String, String)>,
1132    #[serde(default)]
1133    pub tgw_multicast_domains: BTreeMap<String, TgwMulticastDomain>,
1134    #[serde(default)]
1135    pub tgw_metering_policies: BTreeMap<String, TgwMeteringPolicy>,
1136    #[serde(default)]
1137    pub customer_gateways: BTreeMap<String, CustomerGateway>,
1138    #[serde(default)]
1139    pub vpn_gateways: BTreeMap<String, VpnGateway>,
1140    #[serde(default)]
1141    pub vpn_connections: BTreeMap<String, VpnConnection>,
1142    #[serde(default)]
1143    pub vpn_concentrators: BTreeMap<String, VpnConcentrator>,
1144    #[serde(default)]
1145    pub client_vpn_endpoints: BTreeMap<String, ClientVpnEndpoint>,
1146    #[serde(default)]
1147    pub ipams: BTreeMap<String, Ipam>,
1148    #[serde(default)]
1149    pub ipam_scopes: BTreeMap<String, IpamScope>,
1150    #[serde(default)]
1151    pub ipam_pools: BTreeMap<String, IpamPool>,
1152    /// pool-id -> provisioned (cidr, cidr-id).
1153    #[serde(default)]
1154    pub ipam_pool_cidrs: BTreeMap<String, Vec<(String, String)>>,
1155    /// pool-id -> allocations (cidr, allocation-id).
1156    #[serde(default)]
1157    pub ipam_pool_allocations: BTreeMap<String, Vec<(String, String)>>,
1158    #[serde(default)]
1159    pub ipam_resource_discoveries: BTreeMap<String, IpamResourceDiscovery>,
1160    /// association-id -> (discovery-id, ipam-id).
1161    #[serde(default)]
1162    pub ipam_rd_associations: BTreeMap<String, (String, String)>,
1163    /// asn -> associated cidr.
1164    #[serde(default)]
1165    pub ipam_byoasns: BTreeMap<String, String>,
1166    /// external-token-id -> ipam-id.
1167    #[serde(default)]
1168    pub ipam_ext_tokens: BTreeMap<String, String>,
1169    #[serde(default)]
1170    pub ipam_policies: BTreeMap<String, IpamPolicy>,
1171    #[serde(default)]
1172    pub ipam_pl_resolvers: BTreeMap<String, IpamPrefixListResolver>,
1173    #[serde(default)]
1174    pub ipam_pl_resolver_targets: BTreeMap<String, IpamPrefixListResolverTarget>,
1175    /// policy-id -> (locale, resource-type) allocation-rule documents.
1176    #[serde(default)]
1177    pub ipam_policy_alloc_rules: BTreeMap<String, Vec<(String, String)>>,
1178    /// The single enabled IPAM policy id, if any.
1179    #[serde(default)]
1180    pub ipam_enabled_policy: Option<String>,
1181    #[serde(default)]
1182    pub va_instances: BTreeMap<String, VerifiedAccessInstance>,
1183    #[serde(default)]
1184    pub va_trust_providers: BTreeMap<String, VerifiedAccessTrustProvider>,
1185    #[serde(default)]
1186    pub va_groups: BTreeMap<String, VerifiedAccessGroup>,
1187    #[serde(default)]
1188    pub va_endpoints: BTreeMap<String, VerifiedAccessEndpoint>,
1189    /// group-id -> policy document.
1190    #[serde(default)]
1191    pub va_group_policies: BTreeMap<String, String>,
1192    /// endpoint-id -> policy document.
1193    #[serde(default)]
1194    pub va_endpoint_policies: BTreeMap<String, String>,
1195    #[serde(default)]
1196    pub ni_paths: BTreeMap<String, NetworkInsightsPath>,
1197    #[serde(default)]
1198    pub ni_analyses: BTreeMap<String, NetworkInsightsAnalysis>,
1199    #[serde(default)]
1200    pub ni_access_scopes: BTreeMap<String, NetworkInsightsAccessScope>,
1201    #[serde(default)]
1202    pub ni_scope_analyses: BTreeMap<String, NetworkInsightsAccessScopeAnalysis>,
1203    #[serde(default)]
1204    pub carrier_gateways: BTreeMap<String, CarrierGateway>,
1205    #[serde(default)]
1206    pub coip_pools: BTreeMap<String, CoipPool>,
1207    /// coip-pool-id -> CIDRs.
1208    #[serde(default)]
1209    pub coip_pool_cidrs: BTreeMap<String, Vec<String>>,
1210    #[serde(default)]
1211    pub lg_route_tables: BTreeMap<String, LocalGatewayRouteTable>,
1212    /// route-table-id -> destination CIDRs.
1213    #[serde(default)]
1214    pub lg_routes: BTreeMap<String, Vec<String>>,
1215    #[serde(default)]
1216    pub lg_rt_vpc_assocs: BTreeMap<String, LocalGatewayRouteTableVpcAssoc>,
1217    #[serde(default)]
1218    pub lg_virtual_interfaces: BTreeMap<String, LocalGatewayVif>,
1219    #[serde(default)]
1220    pub lg_vif_groups: BTreeMap<String, LocalGatewayVifGroup>,
1221    #[serde(default)]
1222    pub lg_rt_vifg_assocs: BTreeMap<String, LocalGatewayRouteTableVifgAssoc>,
1223    #[serde(default)]
1224    pub instance_connect_endpoints: BTreeMap<String, InstanceConnectEndpoint>,
1225    /// Image ids with fast-launch enabled.
1226    #[serde(default)]
1227    pub fast_launch_images: std::collections::HashSet<String>,
1228    #[serde(default)]
1229    pub serial_console_access: bool,
1230}
1231
1232impl Ec2State {
1233    pub fn new(account_id: &str, region: &str) -> Self {
1234        let mut state = Self {
1235            account_id: account_id.to_string(),
1236            region: region.to_string(),
1237            ..Default::default()
1238        };
1239        // Seed the default VPC topology (VPC, IGW, subnets, route table,
1240        // security group, NACL) the way every AWS account+region ships one, so
1241        // callers that never touch the VPC APIs still launch into a real,
1242        // isolatable network. Ids are deterministic, so the throwaway empty
1243        // states the read paths build report the same ids as this one.
1244        crate::defaults::bootstrap_default_network(&mut state);
1245        state
1246    }
1247
1248    /// Replace the tag set for `resource_id` with `tags` merged over any
1249    /// existing tags (CreateTags is upsert-by-key, matching AWS).
1250    pub fn upsert_tags(&mut self, resource_id: &str, new_tags: &[Tag]) {
1251        let entry = self.tags.entry(resource_id.to_string()).or_default();
1252        for t in new_tags {
1253            if let Some(existing) = entry.iter_mut().find(|e| e.key == t.key) {
1254                existing.value = t.value.clone();
1255            } else {
1256                entry.push(t.clone());
1257            }
1258        }
1259    }
1260
1261    /// Remove tags for `resource_id`. When a tag's value is `None`, the key is
1262    /// removed regardless of value; when `Some`, only a key+value match is
1263    /// removed (AWS DeleteTags semantics).
1264    pub fn remove_tags(&mut self, resource_id: &str, to_remove: &[(String, Option<String>)]) {
1265        if let Some(entry) = self.tags.get_mut(resource_id) {
1266            for (key, value) in to_remove {
1267                entry.retain(|e| {
1268                    if &e.key != key {
1269                        return true;
1270                    }
1271                    match value {
1272                        Some(v) => &e.value != v,
1273                        None => false,
1274                    }
1275                });
1276            }
1277            if entry.is_empty() {
1278                self.tags.remove(resource_id);
1279            }
1280        }
1281    }
1282
1283    /// Tags for `resource_id`, or an empty slice when none.
1284    pub fn tags_for(&self, resource_id: &str) -> &[Tag] {
1285        self.tags.get(resource_id).map(Vec::as_slice).unwrap_or(&[])
1286    }
1287}
1288
1289#[cfg(test)]
1290mod tests {
1291    use super::*;
1292
1293    fn tag(k: &str, v: &str) -> Tag {
1294        Tag {
1295            key: k.to_string(),
1296            value: v.to_string(),
1297        }
1298    }
1299
1300    #[test]
1301    fn upsert_tags_inserts_then_overwrites_by_key() {
1302        let mut s = Ec2State::new("123456789012", "us-east-1");
1303        s.upsert_tags("vpc-1", &[tag("Name", "a"), tag("env", "dev")]);
1304        s.upsert_tags("vpc-1", &[tag("Name", "b")]);
1305        let tags = s.tags_for("vpc-1");
1306        assert_eq!(tags.len(), 2);
1307        assert_eq!(tags.iter().find(|t| t.key == "Name").unwrap().value, "b");
1308    }
1309
1310    #[test]
1311    fn remove_tags_by_key_and_by_key_value() {
1312        let mut s = Ec2State::new("123456789012", "us-east-1");
1313        s.upsert_tags(
1314            "i-1",
1315            &[tag("Name", "x"), tag("env", "prod"), tag("team", "a")],
1316        );
1317        // key-only removal
1318        s.remove_tags("i-1", &[("Name".to_string(), None)]);
1319        // key+value removal that does NOT match -> kept
1320        s.remove_tags("i-1", &[("env".to_string(), Some("dev".to_string()))]);
1321        // key+value removal that matches -> removed
1322        s.remove_tags("i-1", &[("team".to_string(), Some("a".to_string()))]);
1323        let tags = s.tags_for("i-1");
1324        assert_eq!(tags.len(), 1);
1325        assert_eq!(tags[0].key, "env");
1326    }
1327
1328    #[test]
1329    fn empty_tag_set_drops_resource_entry() {
1330        let mut s = Ec2State::new("123456789012", "us-east-1");
1331        s.upsert_tags("sg-1", &[tag("Name", "x")]);
1332        s.remove_tags("sg-1", &[("Name".to_string(), None)]);
1333        assert!(!s.tags.contains_key("sg-1"));
1334    }
1335
1336    fn sample_instance() -> Instance {
1337        Instance {
1338            instance_id: "i-1".to_string(),
1339            image_id: "ami-1".to_string(),
1340            instance_type: "t3.micro".to_string(),
1341            state_code: 16,
1342            state_name: "running".to_string(),
1343            private_ip: "10.0.0.1".to_string(),
1344            public_ip: Some("52.0.0.1".to_string()),
1345            subnet_id: Some("subnet-1".to_string()),
1346            vpc_id: Some("vpc-1".to_string()),
1347            key_name: None,
1348            security_group_ids: vec!["sg-1".to_string()],
1349            reservation_id: "r-1".to_string(),
1350            ami_launch_index: 0,
1351            monitoring: false,
1352            az: "us-east-1a".to_string(),
1353            launch_time: "2024-01-01T00:00:00.000Z".to_string(),
1354            container_id: Some("abc".to_string()),
1355            disable_api_termination: true,
1356            disable_api_stop: true,
1357            source_dest_check: false,
1358            ebs_optimized: true,
1359            instance_initiated_shutdown_behavior: "terminate".to_string(),
1360            user_data: Some("ZWNobyBoaQ==".to_string()),
1361            metadata_options: MetadataOptions {
1362                http_tokens: "required".to_string(),
1363                ..MetadataOptions::default()
1364            },
1365            cpu_options: Some(CpuOptions {
1366                core_count: 4,
1367                threads_per_core: 2,
1368            }),
1369            bandwidth_weighting: Some("vpc-1".to_string()),
1370            maintenance_options: MaintenanceOptions::default(),
1371            placement_tenancy: Some("dedicated".to_string()),
1372            placement_affinity: None,
1373            placement_group_name: Some("cluster-1".to_string()),
1374        }
1375    }
1376
1377    #[test]
1378    fn instance_attributes_round_trip_through_serde() {
1379        let inst = sample_instance();
1380        let json = serde_json::to_string(&inst).unwrap();
1381        let back: Instance = serde_json::from_str(&json).unwrap();
1382        assert!(back.disable_api_termination);
1383        assert!(back.disable_api_stop);
1384        assert!(!back.source_dest_check);
1385        assert!(back.ebs_optimized);
1386        assert_eq!(back.instance_initiated_shutdown_behavior, "terminate");
1387        assert_eq!(back.user_data.as_deref(), Some("ZWNobyBoaQ=="));
1388        assert_eq!(back.metadata_options.http_tokens, "required");
1389        assert_eq!(back.cpu_options.as_ref().unwrap().core_count, 4);
1390        assert_eq!(back.bandwidth_weighting.as_deref(), Some("vpc-1"));
1391        assert_eq!(back.placement_tenancy.as_deref(), Some("dedicated"));
1392        assert_eq!(back.placement_group_name.as_deref(), Some("cluster-1"));
1393    }
1394
1395    #[test]
1396    fn instance_attribute_defaults_load_from_legacy_snapshot() {
1397        // A snapshot written before the attribute fields existed (only the
1398        // pre-existing members) must deserialize, with AWS defaults filled in.
1399        let legacy = r#"{
1400            "instance_id":"i-1","image_id":"ami-1","instance_type":"t3.micro",
1401            "state_code":16,"state_name":"running","private_ip":"10.0.0.1",
1402            "public_ip":null,"subnet_id":null,"vpc_id":null,"key_name":null,
1403            "reservation_id":"r-1","ami_launch_index":0,"monitoring":false,
1404            "az":"us-east-1a","launch_time":"2024-01-01T00:00:00.000Z"
1405        }"#;
1406        let inst: Instance = serde_json::from_str(legacy).unwrap();
1407        assert!(!inst.disable_api_termination);
1408        assert!(inst.source_dest_check, "sourceDestCheck defaults to true");
1409        assert_eq!(inst.instance_initiated_shutdown_behavior, "stop");
1410        assert_eq!(inst.metadata_options.http_tokens, "optional");
1411        assert!(inst.cpu_options.is_none());
1412    }
1413}