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