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