Skip to main content

alien_aws_clients/aws/
ec2.rs

1//! AWS EC2 Client
2//!
3//! This module provides a client for interacting with AWS EC2 APIs, focused on
4//! VPC networking operations including VPCs, Subnets, Internet Gateways, NAT Gateways,
5//! Route Tables, Security Groups, and Elastic IPs.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use alien_aws_clients::ec2::{Ec2Client, Ec2Api, CreateVpcRequest};
11//! use reqwest::Client;
12//!
13//! let ec2_client = Ec2Client::new(Client::new(), aws_config);
14//! let vpc = ec2_client.create_vpc(
15//!     CreateVpcRequest::builder()
16//!         .cidr_block("10.0.0.0/16".to_string())
17//!         .build()
18//! ).await?;
19//! ```
20
21use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
22use crate::aws::credential_provider::AwsCredentialProvider;
23use alien_client_core::{ErrorData, Result};
24use base64::{engine::general_purpose::STANDARD, Engine as _};
25
26use alien_error::ContextError;
27use async_trait::async_trait;
28use bon::Builder;
29use form_urlencoded;
30use reqwest::{Client, Method, StatusCode};
31use serde::de::DeserializeOwned;
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35#[cfg(feature = "test-utils")]
36use mockall::automock;
37
38// ---------------------------------------------------------------------------
39// EC2 Error Response Parsing
40// ---------------------------------------------------------------------------
41
42#[derive(Debug, Deserialize)]
43#[serde(rename_all = "PascalCase")]
44struct Ec2ErrorResponse {
45    pub errors: Ec2ErrorsWrapper,
46}
47
48#[derive(Debug, Deserialize)]
49#[serde(rename_all = "PascalCase")]
50struct Ec2ErrorsWrapper {
51    #[serde(rename = "Error")]
52    pub error: Ec2ErrorDetails,
53}
54
55#[derive(Debug, Deserialize)]
56#[serde(rename_all = "PascalCase")]
57struct Ec2ErrorDetails {
58    pub code: String,
59    pub message: String,
60}
61
62// ---------------------------------------------------------------------------
63// EC2 API Trait
64// ---------------------------------------------------------------------------
65
66#[cfg_attr(feature = "test-utils", automock)]
67#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
68#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
69pub trait Ec2Api: Send + Sync + std::fmt::Debug {
70    // VPC Operations
71    async fn describe_vpcs(&self, request: DescribeVpcsRequest) -> Result<DescribeVpcsResponse>;
72    async fn describe_vpc_attribute(
73        &self,
74        request: DescribeVpcAttributeRequest,
75    ) -> Result<DescribeVpcAttributeResponse>;
76    async fn create_vpc(&self, request: CreateVpcRequest) -> Result<CreateVpcResponse>;
77    async fn delete_vpc(&self, vpc_id: &str) -> Result<()>;
78    async fn modify_vpc_attribute(&self, request: ModifyVpcAttributeRequest) -> Result<()>;
79
80    // Subnet Operations
81    async fn describe_subnets(
82        &self,
83        request: DescribeSubnetsRequest,
84    ) -> Result<DescribeSubnetsResponse>;
85    async fn create_subnet(&self, request: CreateSubnetRequest) -> Result<CreateSubnetResponse>;
86    async fn delete_subnet(&self, subnet_id: &str) -> Result<()>;
87
88    // Internet Gateway Operations
89    async fn create_internet_gateway(
90        &self,
91        request: CreateInternetGatewayRequest,
92    ) -> Result<CreateInternetGatewayResponse>;
93    async fn delete_internet_gateway(&self, internet_gateway_id: &str) -> Result<()>;
94    async fn attach_internet_gateway(&self, request: AttachInternetGatewayRequest) -> Result<()>;
95    async fn detach_internet_gateway(&self, request: DetachInternetGatewayRequest) -> Result<()>;
96    async fn describe_internet_gateways(
97        &self,
98        request: DescribeInternetGatewaysRequest,
99    ) -> Result<DescribeInternetGatewaysResponse>;
100
101    // NAT Gateway Operations
102    async fn create_nat_gateway(
103        &self,
104        request: CreateNatGatewayRequest,
105    ) -> Result<CreateNatGatewayResponse>;
106    async fn delete_nat_gateway(&self, nat_gateway_id: &str) -> Result<DeleteNatGatewayResponse>;
107    async fn describe_nat_gateways(
108        &self,
109        request: DescribeNatGatewaysRequest,
110    ) -> Result<DescribeNatGatewaysResponse>;
111
112    // Elastic IP Operations
113    async fn allocate_address(
114        &self,
115        request: AllocateAddressRequest,
116    ) -> Result<AllocateAddressResponse>;
117    async fn release_address(&self, allocation_id: &str) -> Result<()>;
118
119    // Route Table Operations
120    async fn describe_route_tables(
121        &self,
122        request: DescribeRouteTablesRequest,
123    ) -> Result<DescribeRouteTablesResponse>;
124    async fn create_route_table(
125        &self,
126        request: CreateRouteTableRequest,
127    ) -> Result<CreateRouteTableResponse>;
128    async fn delete_route_table(&self, route_table_id: &str) -> Result<()>;
129    async fn create_route(&self, request: CreateRouteRequest) -> Result<()>;
130    async fn delete_route(&self, request: DeleteRouteRequest) -> Result<()>;
131    async fn associate_route_table(
132        &self,
133        request: AssociateRouteTableRequest,
134    ) -> Result<AssociateRouteTableResponse>;
135    async fn disassociate_route_table(&self, association_id: &str) -> Result<()>;
136
137    // Security Group Operations
138    async fn describe_security_groups(
139        &self,
140        request: DescribeSecurityGroupsRequest,
141    ) -> Result<DescribeSecurityGroupsResponse>;
142    async fn describe_network_interfaces(
143        &self,
144        request: DescribeNetworkInterfacesRequest,
145    ) -> Result<DescribeNetworkInterfacesResponse>;
146    async fn create_security_group(
147        &self,
148        request: CreateSecurityGroupRequest,
149    ) -> Result<CreateSecurityGroupResponse>;
150    async fn delete_security_group(&self, group_id: &str) -> Result<()>;
151    async fn authorize_security_group_ingress(
152        &self,
153        request: AuthorizeSecurityGroupIngressRequest,
154    ) -> Result<()>;
155    async fn authorize_security_group_egress(
156        &self,
157        request: AuthorizeSecurityGroupEgressRequest,
158    ) -> Result<()>;
159    async fn revoke_security_group_ingress(
160        &self,
161        request: RevokeSecurityGroupIngressRequest,
162    ) -> Result<()>;
163    async fn revoke_security_group_egress(
164        &self,
165        request: RevokeSecurityGroupEgressRequest,
166    ) -> Result<()>;
167
168    // Availability Zone Operations
169    async fn describe_availability_zones(
170        &self,
171        request: DescribeAvailabilityZonesRequest,
172    ) -> Result<DescribeAvailabilityZonesResponse>;
173
174    // AMI Operations
175    async fn describe_images(
176        &self,
177        request: DescribeImagesRequest,
178    ) -> Result<DescribeImagesResponse>;
179
180    // Instance Operations
181    async fn terminate_instances(
182        &self,
183        instance_ids: Vec<String>,
184    ) -> Result<TerminateInstancesResponse>;
185    async fn describe_instances(
186        &self,
187        request: DescribeInstancesRequest,
188    ) -> Result<DescribeInstancesResponse>;
189
190    // Volume Operations
191    async fn create_volume(&self, request: CreateVolumeRequest) -> Result<CreateVolumeResponse>;
192    async fn modify_volume(&self, request: ModifyVolumeRequest) -> Result<ModifyVolumeResponse>;
193    async fn describe_volumes_modifications(
194        &self,
195        request: DescribeVolumesModificationsRequest,
196    ) -> Result<DescribeVolumesModificationsResponse>;
197    async fn delete_volume(&self, volume_id: &str) -> Result<()>;
198    async fn describe_volumes(
199        &self,
200        request: DescribeVolumesRequest,
201    ) -> Result<DescribeVolumesResponse>;
202    async fn attach_volume(&self, request: AttachVolumeRequest) -> Result<AttachVolumeResponse>;
203    async fn detach_volume(&self, request: DetachVolumeRequest) -> Result<DetachVolumeResponse>;
204
205    // Launch Template Operations
206    async fn create_launch_template(
207        &self,
208        request: CreateLaunchTemplateRequest,
209    ) -> Result<CreateLaunchTemplateResponse>;
210    /// Creates a new version of an existing launch template with updated launch template data.
211    /// ASGs using $Latest will automatically pick up the new version.
212    /// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateLaunchTemplateVersion.html
213    async fn create_launch_template_version(
214        &self,
215        request: CreateLaunchTemplateVersionRequest,
216    ) -> Result<CreateLaunchTemplateVersionResponse>;
217    async fn delete_launch_template(
218        &self,
219        request: DeleteLaunchTemplateRequest,
220    ) -> Result<DeleteLaunchTemplateResponse>;
221    async fn describe_launch_templates(
222        &self,
223        request: DescribeLaunchTemplatesRequest,
224    ) -> Result<DescribeLaunchTemplatesResponse>;
225
226    // Console Output
227    /// Gets the console output for an EC2 instance (base64-encoded).
228    /// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetConsoleOutput.html
229    async fn get_console_output(&self, instance_id: String) -> Result<GetConsoleOutputResponse>;
230}
231
232// ---------------------------------------------------------------------------
233// EC2 Client
234// ---------------------------------------------------------------------------
235
236#[derive(Debug, Clone)]
237pub struct Ec2Client {
238    client: Client,
239    credentials: AwsCredentialProvider,
240}
241
242impl Ec2Client {
243    /// Create a new EC2 client.
244    pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
245        Self {
246            client,
247            credentials,
248        }
249    }
250
251    fn sign_config(&self) -> AwsSignConfig {
252        AwsSignConfig {
253            service_name: "ec2".into(),
254            region: self.credentials.region().to_string(),
255            credentials: self.credentials.get_credentials(),
256            signing_region: None,
257        }
258    }
259
260    fn get_base_url(&self) -> String {
261        if let Some(override_url) = self.credentials.get_service_endpoint_option("ec2") {
262            override_url.to_string()
263        } else {
264            format!("https://ec2.{}.amazonaws.com", self.credentials.region())
265        }
266    }
267
268    fn get_host(&self) -> String {
269        format!("ec2.{}.amazonaws.com", self.credentials.region())
270    }
271
272    // ------------------------- Internal Helpers -------------------------
273
274    async fn send_form<T: DeserializeOwned + Send + 'static>(
275        &self,
276        form_data: HashMap<String, String>,
277        operation: &str,
278        resource: &str,
279    ) -> Result<T> {
280        self.credentials.ensure_fresh().await?;
281        let url = self.get_base_url();
282
283        let form_body = form_urlencoded::Serializer::new(String::new())
284            .extend_pairs(form_data.iter())
285            .finish();
286
287        let builder = self
288            .client
289            .request(Method::POST, &url)
290            .host(&self.get_host())
291            .content_type_form()
292            .content_sha256(&form_body)
293            .body(form_body.clone());
294
295        let result =
296            crate::aws::aws_request_utils::sign_send_xml(builder, &self.sign_config()).await;
297
298        Self::map_result(result, operation, resource, Some(&form_body))
299    }
300
301    async fn send_form_no_body(
302        &self,
303        form_data: HashMap<String, String>,
304        operation: &str,
305        resource: &str,
306    ) -> Result<()> {
307        self.credentials.ensure_fresh().await?;
308        let url = self.get_base_url();
309
310        let form_body = form_urlencoded::Serializer::new(String::new())
311            .extend_pairs(form_data.iter())
312            .finish();
313
314        let builder = self
315            .client
316            .request(Method::POST, &url)
317            .host(&self.get_host())
318            .content_type_form()
319            .content_sha256(&form_body)
320            .body(form_body.clone());
321
322        let result =
323            crate::aws::aws_request_utils::sign_send_no_response(builder, &self.sign_config())
324                .await;
325
326        Self::map_result(result, operation, resource, Some(&form_body))
327    }
328
329    fn map_result<T>(
330        result: Result<T>,
331        operation: &str,
332        resource: &str,
333        request_body: Option<&str>,
334    ) -> Result<T> {
335        match result {
336            Ok(v) => Ok(v),
337            Err(e) => {
338                if let Some(ErrorData::HttpResponseError {
339                    http_status,
340                    http_response_text: Some(ref text),
341                    ..
342                }) = &e.error
343                {
344                    let status = StatusCode::from_u16(*http_status)
345                        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
346                    if let Some(mapped) =
347                        Self::map_ec2_error(status, text, operation, resource, request_body)
348                    {
349                        Err(e.context(mapped))
350                    } else {
351                        Err(e)
352                    }
353                } else {
354                    Err(e)
355                }
356            }
357        }
358    }
359
360    fn map_ec2_error(
361        status: StatusCode,
362        body: &str,
363        _operation: &str,
364        resource: &str,
365        request_body: Option<&str>,
366    ) -> Option<ErrorData> {
367        // Handle empty response bodies
368        if body.trim().is_empty() {
369            return match status {
370                StatusCode::NOT_FOUND => Some(ErrorData::RemoteResourceNotFound {
371                    resource_type: "EC2 Resource".into(),
372                    resource_name: resource.into(),
373                }),
374                StatusCode::CONFLICT => Some(ErrorData::RemoteResourceConflict {
375                    message: "Resource conflict".into(),
376                    resource_type: "EC2 Resource".into(),
377                    resource_name: resource.into(),
378                }),
379                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => {
380                    Some(ErrorData::RemoteAccessDenied {
381                        resource_type: "EC2 Resource".into(),
382                        resource_name: resource.into(),
383                    })
384                }
385                StatusCode::TOO_MANY_REQUESTS => Some(ErrorData::RateLimitExceeded {
386                    message: "Too many requests".into(),
387                }),
388                StatusCode::SERVICE_UNAVAILABLE
389                | StatusCode::BAD_GATEWAY
390                | StatusCode::GATEWAY_TIMEOUT => Some(ErrorData::RemoteServiceUnavailable {
391                    message: "Service unavailable".into(),
392                }),
393                _ => None,
394            };
395        }
396
397        // Try to parse EC2 error XML
398        let parsed: std::result::Result<Ec2ErrorResponse, _> = quick_xml::de::from_str(body);
399        let (code, message) = match parsed {
400            Ok(e) => (e.errors.error.code, e.errors.error.message),
401            Err(_) => {
402                // If we can't parse the response, fall back to status code mapping
403                let default_message = "Unknown error".to_string();
404                return match status {
405                    StatusCode::NOT_FOUND => Some(ErrorData::RemoteResourceNotFound {
406                        resource_type: "EC2 Resource".into(),
407                        resource_name: resource.into(),
408                    }),
409                    StatusCode::CONFLICT => Some(ErrorData::RemoteResourceConflict {
410                        message: default_message,
411                        resource_type: "EC2 Resource".into(),
412                        resource_name: resource.into(),
413                    }),
414                    StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => {
415                        Some(ErrorData::RemoteAccessDenied {
416                            resource_type: "EC2 Resource".into(),
417                            resource_name: resource.into(),
418                        })
419                    }
420                    StatusCode::TOO_MANY_REQUESTS => Some(ErrorData::RateLimitExceeded {
421                        message: default_message,
422                    }),
423                    StatusCode::SERVICE_UNAVAILABLE
424                    | StatusCode::BAD_GATEWAY
425                    | StatusCode::GATEWAY_TIMEOUT => Some(ErrorData::RemoteServiceUnavailable {
426                        message: default_message,
427                    }),
428                    _ => None,
429                };
430            }
431        };
432
433        // Map EC2 error codes to our error types
434        // Reference: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html
435        Some(match code.as_str() {
436            // Access / Auth errors
437            "AuthFailure" | "UnauthorizedOperation" | "Blocked" => ErrorData::RemoteAccessDenied {
438                resource_type: "EC2 Resource".into(),
439                resource_name: resource.into(),
440            },
441            // Throttling
442            "RequestLimitExceeded" | "ResourceLimitExceeded" => {
443                ErrorData::RateLimitExceeded { message }
444            }
445            // Service unavailable
446            "ServiceUnavailable" | "Unavailable" | "InternalError" | "InternalFailure" => {
447                ErrorData::RemoteServiceUnavailable { message }
448            }
449            // Resource not found errors
450            "InvalidVpcID.NotFound" | "InvalidVpc.NotFound" => ErrorData::RemoteResourceNotFound {
451                resource_type: "VPC".into(),
452                resource_name: resource.into(),
453            },
454            "InvalidSubnetID.NotFound" | "InvalidSubnet.NotFound" => {
455                ErrorData::RemoteResourceNotFound {
456                    resource_type: "Subnet".into(),
457                    resource_name: resource.into(),
458                }
459            }
460            "InvalidInternetGatewayID.NotFound" | "InvalidInternetGateway.NotFound" => {
461                ErrorData::RemoteResourceNotFound {
462                    resource_type: "InternetGateway".into(),
463                    resource_name: resource.into(),
464                }
465            }
466            "InvalidNatGatewayID.NotFound" | "NatGatewayNotFound" => {
467                ErrorData::RemoteResourceNotFound {
468                    resource_type: "NatGateway".into(),
469                    resource_name: resource.into(),
470                }
471            }
472            "InvalidRouteTableID.NotFound" | "InvalidRouteTableId.NotFound" => {
473                ErrorData::RemoteResourceNotFound {
474                    resource_type: "RouteTable".into(),
475                    resource_name: resource.into(),
476                }
477            }
478            "InvalidGroup.NotFound" | "InvalidSecurityGroupID.NotFound" => {
479                ErrorData::RemoteResourceNotFound {
480                    resource_type: "SecurityGroup".into(),
481                    resource_name: resource.into(),
482                }
483            }
484            "InvalidAllocationID.NotFound" | "InvalidAddress.NotFound" => {
485                ErrorData::RemoteResourceNotFound {
486                    resource_type: "ElasticIP".into(),
487                    resource_name: resource.into(),
488                }
489            }
490            "InvalidAssociationID.NotFound" => ErrorData::RemoteResourceNotFound {
491                resource_type: "RouteTableAssociation".into(),
492                resource_name: resource.into(),
493            },
494            "InvalidVolume.NotFound" | "InvalidVolumeID.NotFound" => {
495                ErrorData::RemoteResourceNotFound {
496                    resource_type: "Volume".into(),
497                    resource_name: resource.into(),
498                }
499            }
500            // Conflict / already exists errors
501            "VpcLimitExceeded"
502            | "SubnetLimitExceeded"
503            | "SecurityGroupLimitExceeded"
504            | "AddressLimitExceeded" => ErrorData::QuotaExceeded { message },
505            "InvalidVpcState"
506            | "InvalidSubnetID.DuplicateSubnet"
507            | "InvalidGroup.Duplicate"
508            | "InvalidPermission.Duplicate"
509            | "InvalidLaunchTemplateName.AlreadyExistsException" => {
510                ErrorData::RemoteResourceConflict {
511                    message,
512                    resource_type: "EC2 Resource".into(),
513                    resource_name: resource.into(),
514                }
515            }
516            "DependencyViolation" | "ResourceInUse" => ErrorData::RemoteResourceConflict {
517                message,
518                resource_type: "EC2 Resource".into(),
519                resource_name: resource.into(),
520            },
521            "Gateway.NotAttached" => ErrorData::RemoteResourceConflict {
522                message,
523                resource_type: "InternetGateway".into(),
524                resource_name: resource.into(),
525            },
526            "RouteAlreadyExists" => ErrorData::RemoteResourceConflict {
527                message,
528                resource_type: "Route".into(),
529                resource_name: resource.into(),
530            },
531            // Invalid input errors
532            "InvalidParameterValue" | "InvalidParameter" | "MissingParameter" | "InvalidInput" => {
533                ErrorData::InvalidInput {
534                    message,
535                    field_name: None,
536                }
537            }
538            "InvalidVpcRange" | "InvalidSubnetRange.Conflict" | "InvalidCIDRBlock" => {
539                ErrorData::InvalidInput {
540                    message,
541                    field_name: Some("cidr_block".into()),
542                }
543            }
544            // Default fallback based on status code
545            _ => match status {
546                StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
547                    resource_type: "EC2 Resource".into(),
548                    resource_name: resource.into(),
549                },
550                StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
551                    message,
552                    resource_type: "EC2 Resource".into(),
553                    resource_name: resource.into(),
554                },
555                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
556                    resource_type: "EC2 Resource".into(),
557                    resource_name: resource.into(),
558                },
559                StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
560                StatusCode::SERVICE_UNAVAILABLE
561                | StatusCode::BAD_GATEWAY
562                | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
563                _ => ErrorData::HttpResponseError {
564                    message: format!("EC2 operation failed: {}", message),
565                    url: "ec2.amazonaws.com".into(),
566                    http_status: status.as_u16(),
567                    http_response_text: Some(body.into()),
568                    http_request_text: request_body.map(|s| s.to_string()),
569                },
570            },
571        })
572    }
573
574    /// Add filter parameters to the form data.
575    fn add_filters(form_data: &mut HashMap<String, String>, filters: &[Filter]) {
576        for (i, filter) in filters.iter().enumerate() {
577            let idx = i + 1;
578            form_data.insert(format!("Filter.{}.Name", idx), filter.name.clone());
579            for (j, value) in filter.values.iter().enumerate() {
580                form_data.insert(format!("Filter.{}.Value.{}", idx, j + 1), value.clone());
581            }
582        }
583    }
584
585    /// Add tag specifications to the form data.
586    fn add_tag_specifications(
587        form_data: &mut HashMap<String, String>,
588        tag_specs: &[TagSpecification],
589    ) {
590        Self::add_tag_specifications_with_prefix(form_data, "TagSpecification", tag_specs);
591    }
592
593    /// Add tag specification parameters under a specific EC2 query prefix.
594    fn add_tag_specifications_with_prefix(
595        form_data: &mut HashMap<String, String>,
596        prefix: &str,
597        tag_specs: &[TagSpecification],
598    ) {
599        for (i, spec) in tag_specs.iter().enumerate() {
600            let idx = i + 1;
601            form_data.insert(
602                format!("{prefix}.{idx}.ResourceType"),
603                spec.resource_type.clone(),
604            );
605            for (j, tag) in spec.tags.iter().enumerate() {
606                form_data.insert(format!("{prefix}.{idx}.Tag.{}.Key", j + 1), tag.key.clone());
607                form_data.insert(
608                    format!("{prefix}.{idx}.Tag.{}.Value", j + 1),
609                    tag.value.clone(),
610                );
611            }
612        }
613    }
614
615    fn create_volume_form_data(request: &CreateVolumeRequest) -> HashMap<String, String> {
616        let mut form_data = HashMap::new();
617        form_data.insert("Action".to_string(), "CreateVolume".to_string());
618        form_data.insert("Version".to_string(), "2016-11-15".to_string());
619        form_data.insert(
620            "AvailabilityZone".to_string(),
621            request.availability_zone.clone(),
622        );
623
624        if let Some(client_token) = &request.client_token {
625            form_data.insert("ClientToken".to_string(), client_token.clone());
626        }
627        if let Some(size) = request.size {
628            form_data.insert("Size".to_string(), size.to_string());
629        }
630        if let Some(snapshot_id) = &request.snapshot_id {
631            form_data.insert("SnapshotId".to_string(), snapshot_id.clone());
632        }
633        if let Some(volume_type) = &request.volume_type {
634            form_data.insert("VolumeType".to_string(), volume_type.clone());
635        }
636        if let Some(iops) = request.iops {
637            form_data.insert("Iops".to_string(), iops.to_string());
638        }
639        if let Some(throughput) = request.throughput {
640            form_data.insert("Throughput".to_string(), throughput.to_string());
641        }
642        if let Some(encrypted) = request.encrypted {
643            form_data.insert("Encrypted".to_string(), encrypted.to_string());
644        }
645        if let Some(kms_key_id) = &request.kms_key_id {
646            form_data.insert("KmsKeyId".to_string(), kms_key_id.clone());
647        }
648        if let Some(tag_specs) = &request.tag_specifications {
649            Self::add_tag_specifications(&mut form_data, tag_specs);
650        }
651
652        form_data
653    }
654
655    fn modify_volume_form_data(request: &ModifyVolumeRequest) -> HashMap<String, String> {
656        let mut form_data = HashMap::new();
657        form_data.insert("Action".to_string(), "ModifyVolume".to_string());
658        form_data.insert("Version".to_string(), "2016-11-15".to_string());
659        form_data.insert("VolumeId".to_string(), request.volume_id.clone());
660        form_data.insert("Size".to_string(), request.size.to_string());
661        form_data
662    }
663
664    fn describe_volumes_modifications_form_data(
665        request: &DescribeVolumesModificationsRequest,
666    ) -> HashMap<String, String> {
667        let mut form_data = HashMap::new();
668        form_data.insert(
669            "Action".to_string(),
670            "DescribeVolumesModifications".to_string(),
671        );
672        form_data.insert("Version".to_string(), "2016-11-15".to_string());
673
674        if let Some(volume_ids) = &request.volume_ids {
675            for (i, volume_id) in volume_ids.iter().enumerate() {
676                form_data.insert(format!("VolumeId.{}", i + 1), volume_id.clone());
677            }
678        }
679        if let Some(filters) = &request.filters {
680            Self::add_filters(&mut form_data, filters);
681        }
682        if let Some(max_results) = request.max_results {
683            form_data.insert("MaxResults".to_string(), max_results.to_string());
684        }
685        if let Some(next_token) = &request.next_token {
686            form_data.insert("NextToken".to_string(), next_token.clone());
687        }
688
689        form_data
690    }
691}
692
693#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
694#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
695impl Ec2Api for Ec2Client {
696    // ---------------------------------------------------------------------------
697    // VPC Operations
698    // ---------------------------------------------------------------------------
699
700    async fn describe_vpcs(&self, request: DescribeVpcsRequest) -> Result<DescribeVpcsResponse> {
701        let mut form_data = HashMap::new();
702        form_data.insert("Action".to_string(), "DescribeVpcs".to_string());
703        form_data.insert("Version".to_string(), "2016-11-15".to_string());
704
705        if let Some(vpc_ids) = &request.vpc_ids {
706            for (i, vpc_id) in vpc_ids.iter().enumerate() {
707                form_data.insert(format!("VpcId.{}", i + 1), vpc_id.clone());
708            }
709        }
710
711        if let Some(filters) = &request.filters {
712            Self::add_filters(&mut form_data, filters);
713        }
714
715        if let Some(max_results) = request.max_results {
716            form_data.insert("MaxResults".to_string(), max_results.to_string());
717        }
718
719        if let Some(next_token) = &request.next_token {
720            form_data.insert("NextToken".to_string(), next_token.clone());
721        }
722
723        self.send_form(form_data, "DescribeVpcs", "VPC").await
724    }
725
726    async fn describe_vpc_attribute(
727        &self,
728        request: DescribeVpcAttributeRequest,
729    ) -> Result<DescribeVpcAttributeResponse> {
730        let mut form_data = HashMap::new();
731        form_data.insert("Action".to_string(), "DescribeVpcAttribute".to_string());
732        form_data.insert("Version".to_string(), "2016-11-15".to_string());
733        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
734        form_data.insert("Attribute".to_string(), request.attribute.clone());
735
736        self.send_form(form_data, "DescribeVpcAttribute", &request.vpc_id)
737            .await
738    }
739
740    async fn create_vpc(&self, request: CreateVpcRequest) -> Result<CreateVpcResponse> {
741        let mut form_data = HashMap::new();
742        form_data.insert("Action".to_string(), "CreateVpc".to_string());
743        form_data.insert("Version".to_string(), "2016-11-15".to_string());
744        form_data.insert("CidrBlock".to_string(), request.cidr_block.clone());
745
746        if let Some(instance_tenancy) = &request.instance_tenancy {
747            form_data.insert("InstanceTenancy".to_string(), instance_tenancy.clone());
748        }
749
750        if let Some(amazon_provided_ipv6_cidr_block) = request.amazon_provided_ipv6_cidr_block {
751            form_data.insert(
752                "AmazonProvidedIpv6CidrBlock".to_string(),
753                amazon_provided_ipv6_cidr_block.to_string(),
754            );
755        }
756
757        if let Some(tag_specs) = &request.tag_specifications {
758            Self::add_tag_specifications(&mut form_data, tag_specs);
759        }
760
761        self.send_form(form_data, "CreateVpc", &request.cidr_block)
762            .await
763    }
764
765    async fn delete_vpc(&self, vpc_id: &str) -> Result<()> {
766        let mut form_data = HashMap::new();
767        form_data.insert("Action".to_string(), "DeleteVpc".to_string());
768        form_data.insert("Version".to_string(), "2016-11-15".to_string());
769        form_data.insert("VpcId".to_string(), vpc_id.to_string());
770
771        self.send_form_no_body(form_data, "DeleteVpc", vpc_id).await
772    }
773
774    async fn modify_vpc_attribute(&self, request: ModifyVpcAttributeRequest) -> Result<()> {
775        let mut form_data = HashMap::new();
776        form_data.insert("Action".to_string(), "ModifyVpcAttribute".to_string());
777        form_data.insert("Version".to_string(), "2016-11-15".to_string());
778        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
779
780        if let Some(enable_dns_support) = request.enable_dns_support {
781            form_data.insert(
782                "EnableDnsSupport.Value".to_string(),
783                enable_dns_support.to_string(),
784            );
785        }
786
787        if let Some(enable_dns_hostnames) = request.enable_dns_hostnames {
788            form_data.insert(
789                "EnableDnsHostnames.Value".to_string(),
790                enable_dns_hostnames.to_string(),
791            );
792        }
793
794        self.send_form_no_body(form_data, "ModifyVpcAttribute", &request.vpc_id)
795            .await
796    }
797
798    // ---------------------------------------------------------------------------
799    // Subnet Operations
800    // ---------------------------------------------------------------------------
801
802    async fn describe_subnets(
803        &self,
804        request: DescribeSubnetsRequest,
805    ) -> Result<DescribeSubnetsResponse> {
806        let mut form_data = HashMap::new();
807        form_data.insert("Action".to_string(), "DescribeSubnets".to_string());
808        form_data.insert("Version".to_string(), "2016-11-15".to_string());
809
810        if let Some(subnet_ids) = &request.subnet_ids {
811            for (i, subnet_id) in subnet_ids.iter().enumerate() {
812                form_data.insert(format!("SubnetId.{}", i + 1), subnet_id.clone());
813            }
814        }
815
816        if let Some(filters) = &request.filters {
817            Self::add_filters(&mut form_data, filters);
818        }
819
820        if let Some(max_results) = request.max_results {
821            form_data.insert("MaxResults".to_string(), max_results.to_string());
822        }
823
824        if let Some(next_token) = &request.next_token {
825            form_data.insert("NextToken".to_string(), next_token.clone());
826        }
827
828        self.send_form(form_data, "DescribeSubnets", "Subnet").await
829    }
830
831    async fn create_subnet(&self, request: CreateSubnetRequest) -> Result<CreateSubnetResponse> {
832        let mut form_data = HashMap::new();
833        form_data.insert("Action".to_string(), "CreateSubnet".to_string());
834        form_data.insert("Version".to_string(), "2016-11-15".to_string());
835        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
836        form_data.insert("CidrBlock".to_string(), request.cidr_block.clone());
837
838        if let Some(availability_zone) = &request.availability_zone {
839            form_data.insert("AvailabilityZone".to_string(), availability_zone.clone());
840        }
841
842        if let Some(availability_zone_id) = &request.availability_zone_id {
843            form_data.insert(
844                "AvailabilityZoneId".to_string(),
845                availability_zone_id.clone(),
846            );
847        }
848
849        if let Some(tag_specs) = &request.tag_specifications {
850            Self::add_tag_specifications(&mut form_data, tag_specs);
851        }
852
853        self.send_form(form_data, "CreateSubnet", &request.cidr_block)
854            .await
855    }
856
857    async fn delete_subnet(&self, subnet_id: &str) -> Result<()> {
858        let mut form_data = HashMap::new();
859        form_data.insert("Action".to_string(), "DeleteSubnet".to_string());
860        form_data.insert("Version".to_string(), "2016-11-15".to_string());
861        form_data.insert("SubnetId".to_string(), subnet_id.to_string());
862
863        self.send_form_no_body(form_data, "DeleteSubnet", subnet_id)
864            .await
865    }
866
867    // ---------------------------------------------------------------------------
868    // Internet Gateway Operations
869    // ---------------------------------------------------------------------------
870
871    async fn create_internet_gateway(
872        &self,
873        request: CreateInternetGatewayRequest,
874    ) -> Result<CreateInternetGatewayResponse> {
875        let mut form_data = HashMap::new();
876        form_data.insert("Action".to_string(), "CreateInternetGateway".to_string());
877        form_data.insert("Version".to_string(), "2016-11-15".to_string());
878
879        if let Some(tag_specs) = &request.tag_specifications {
880            Self::add_tag_specifications(&mut form_data, tag_specs);
881        }
882
883        self.send_form(form_data, "CreateInternetGateway", "InternetGateway")
884            .await
885    }
886
887    async fn delete_internet_gateway(&self, internet_gateway_id: &str) -> Result<()> {
888        let mut form_data = HashMap::new();
889        form_data.insert("Action".to_string(), "DeleteInternetGateway".to_string());
890        form_data.insert("Version".to_string(), "2016-11-15".to_string());
891        form_data.insert(
892            "InternetGatewayId".to_string(),
893            internet_gateway_id.to_string(),
894        );
895
896        self.send_form_no_body(form_data, "DeleteInternetGateway", internet_gateway_id)
897            .await
898    }
899
900    async fn attach_internet_gateway(&self, request: AttachInternetGatewayRequest) -> Result<()> {
901        let mut form_data = HashMap::new();
902        form_data.insert("Action".to_string(), "AttachInternetGateway".to_string());
903        form_data.insert("Version".to_string(), "2016-11-15".to_string());
904        form_data.insert(
905            "InternetGatewayId".to_string(),
906            request.internet_gateway_id.clone(),
907        );
908        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
909
910        self.send_form_no_body(
911            form_data,
912            "AttachInternetGateway",
913            &request.internet_gateway_id,
914        )
915        .await
916    }
917
918    async fn detach_internet_gateway(&self, request: DetachInternetGatewayRequest) -> Result<()> {
919        let mut form_data = HashMap::new();
920        form_data.insert("Action".to_string(), "DetachInternetGateway".to_string());
921        form_data.insert("Version".to_string(), "2016-11-15".to_string());
922        form_data.insert(
923            "InternetGatewayId".to_string(),
924            request.internet_gateway_id.clone(),
925        );
926        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
927
928        self.send_form_no_body(
929            form_data,
930            "DetachInternetGateway",
931            &request.internet_gateway_id,
932        )
933        .await
934    }
935
936    async fn describe_internet_gateways(
937        &self,
938        request: DescribeInternetGatewaysRequest,
939    ) -> Result<DescribeInternetGatewaysResponse> {
940        let mut form_data = HashMap::new();
941        form_data.insert("Action".to_string(), "DescribeInternetGateways".to_string());
942        form_data.insert("Version".to_string(), "2016-11-15".to_string());
943
944        if let Some(igw_ids) = &request.internet_gateway_ids {
945            for (i, igw_id) in igw_ids.iter().enumerate() {
946                form_data.insert(format!("InternetGatewayId.{}", i + 1), igw_id.clone());
947            }
948        }
949
950        if let Some(filters) = &request.filters {
951            Self::add_filters(&mut form_data, filters);
952        }
953
954        if let Some(max_results) = request.max_results {
955            form_data.insert("MaxResults".to_string(), max_results.to_string());
956        }
957
958        if let Some(next_token) = &request.next_token {
959            form_data.insert("NextToken".to_string(), next_token.clone());
960        }
961
962        self.send_form(form_data, "DescribeInternetGateways", "InternetGateway")
963            .await
964    }
965
966    // ---------------------------------------------------------------------------
967    // NAT Gateway Operations
968    // ---------------------------------------------------------------------------
969
970    async fn create_nat_gateway(
971        &self,
972        request: CreateNatGatewayRequest,
973    ) -> Result<CreateNatGatewayResponse> {
974        let mut form_data = HashMap::new();
975        form_data.insert("Action".to_string(), "CreateNatGateway".to_string());
976        form_data.insert("Version".to_string(), "2016-11-15".to_string());
977        form_data.insert("SubnetId".to_string(), request.subnet_id.clone());
978
979        if let Some(allocation_id) = &request.allocation_id {
980            form_data.insert("AllocationId".to_string(), allocation_id.clone());
981        }
982
983        if let Some(connectivity_type) = &request.connectivity_type {
984            form_data.insert("ConnectivityType".to_string(), connectivity_type.clone());
985        }
986
987        if let Some(private_ip_address) = &request.private_ip_address {
988            form_data.insert("PrivateIpAddress".to_string(), private_ip_address.clone());
989        }
990
991        if let Some(tag_specs) = &request.tag_specifications {
992            Self::add_tag_specifications(&mut form_data, tag_specs);
993        }
994
995        self.send_form(form_data, "CreateNatGateway", &request.subnet_id)
996            .await
997    }
998
999    async fn delete_nat_gateway(&self, nat_gateway_id: &str) -> Result<DeleteNatGatewayResponse> {
1000        let mut form_data = HashMap::new();
1001        form_data.insert("Action".to_string(), "DeleteNatGateway".to_string());
1002        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1003        form_data.insert("NatGatewayId".to_string(), nat_gateway_id.to_string());
1004
1005        self.send_form(form_data, "DeleteNatGateway", nat_gateway_id)
1006            .await
1007    }
1008
1009    async fn describe_nat_gateways(
1010        &self,
1011        request: DescribeNatGatewaysRequest,
1012    ) -> Result<DescribeNatGatewaysResponse> {
1013        let mut form_data = HashMap::new();
1014        form_data.insert("Action".to_string(), "DescribeNatGateways".to_string());
1015        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1016
1017        if let Some(nat_gateway_ids) = &request.nat_gateway_ids {
1018            for (i, nat_id) in nat_gateway_ids.iter().enumerate() {
1019                form_data.insert(format!("NatGatewayId.{}", i + 1), nat_id.clone());
1020            }
1021        }
1022
1023        if let Some(filters) = &request.filters {
1024            Self::add_filters(&mut form_data, filters);
1025        }
1026
1027        if let Some(max_results) = request.max_results {
1028            form_data.insert("MaxResults".to_string(), max_results.to_string());
1029        }
1030
1031        if let Some(next_token) = &request.next_token {
1032            form_data.insert("NextToken".to_string(), next_token.clone());
1033        }
1034
1035        self.send_form(form_data, "DescribeNatGateways", "NatGateway")
1036            .await
1037    }
1038
1039    // ---------------------------------------------------------------------------
1040    // Elastic IP Operations
1041    // ---------------------------------------------------------------------------
1042
1043    async fn allocate_address(
1044        &self,
1045        request: AllocateAddressRequest,
1046    ) -> Result<AllocateAddressResponse> {
1047        let mut form_data = HashMap::new();
1048        form_data.insert("Action".to_string(), "AllocateAddress".to_string());
1049        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1050
1051        // Default to VPC domain
1052        let domain = request.domain.as_deref().unwrap_or("vpc");
1053        form_data.insert("Domain".to_string(), domain.to_string());
1054
1055        if let Some(tag_specs) = &request.tag_specifications {
1056            Self::add_tag_specifications(&mut form_data, tag_specs);
1057        }
1058
1059        self.send_form(form_data, "AllocateAddress", "ElasticIP")
1060            .await
1061    }
1062
1063    async fn release_address(&self, allocation_id: &str) -> Result<()> {
1064        let mut form_data = HashMap::new();
1065        form_data.insert("Action".to_string(), "ReleaseAddress".to_string());
1066        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1067        form_data.insert("AllocationId".to_string(), allocation_id.to_string());
1068
1069        self.send_form_no_body(form_data, "ReleaseAddress", allocation_id)
1070            .await
1071    }
1072
1073    // ---------------------------------------------------------------------------
1074    // Route Table Operations
1075    // ---------------------------------------------------------------------------
1076
1077    async fn describe_route_tables(
1078        &self,
1079        request: DescribeRouteTablesRequest,
1080    ) -> Result<DescribeRouteTablesResponse> {
1081        let mut form_data = HashMap::new();
1082        form_data.insert("Action".to_string(), "DescribeRouteTables".to_string());
1083        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1084
1085        if let Some(rt_ids) = &request.route_table_ids {
1086            for (i, rt_id) in rt_ids.iter().enumerate() {
1087                form_data.insert(format!("RouteTableId.{}", i + 1), rt_id.clone());
1088            }
1089        }
1090
1091        if let Some(filters) = &request.filters {
1092            Self::add_filters(&mut form_data, filters);
1093        }
1094
1095        if let Some(max_results) = request.max_results {
1096            form_data.insert("MaxResults".to_string(), max_results.to_string());
1097        }
1098
1099        if let Some(next_token) = &request.next_token {
1100            form_data.insert("NextToken".to_string(), next_token.clone());
1101        }
1102
1103        self.send_form(form_data, "DescribeRouteTables", "RouteTable")
1104            .await
1105    }
1106
1107    async fn create_route_table(
1108        &self,
1109        request: CreateRouteTableRequest,
1110    ) -> Result<CreateRouteTableResponse> {
1111        let mut form_data = HashMap::new();
1112        form_data.insert("Action".to_string(), "CreateRouteTable".to_string());
1113        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1114        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
1115
1116        if let Some(tag_specs) = &request.tag_specifications {
1117            Self::add_tag_specifications(&mut form_data, tag_specs);
1118        }
1119
1120        self.send_form(form_data, "CreateRouteTable", &request.vpc_id)
1121            .await
1122    }
1123
1124    async fn delete_route_table(&self, route_table_id: &str) -> Result<()> {
1125        let mut form_data = HashMap::new();
1126        form_data.insert("Action".to_string(), "DeleteRouteTable".to_string());
1127        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1128        form_data.insert("RouteTableId".to_string(), route_table_id.to_string());
1129
1130        self.send_form_no_body(form_data, "DeleteRouteTable", route_table_id)
1131            .await
1132    }
1133
1134    async fn create_route(&self, request: CreateRouteRequest) -> Result<()> {
1135        let mut form_data = HashMap::new();
1136        form_data.insert("Action".to_string(), "CreateRoute".to_string());
1137        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1138        form_data.insert("RouteTableId".to_string(), request.route_table_id.clone());
1139        form_data.insert(
1140            "DestinationCidrBlock".to_string(),
1141            request.destination_cidr_block.clone(),
1142        );
1143
1144        if let Some(gateway_id) = &request.gateway_id {
1145            form_data.insert("GatewayId".to_string(), gateway_id.clone());
1146        }
1147
1148        if let Some(nat_gateway_id) = &request.nat_gateway_id {
1149            form_data.insert("NatGatewayId".to_string(), nat_gateway_id.clone());
1150        }
1151
1152        if let Some(instance_id) = &request.instance_id {
1153            form_data.insert("InstanceId".to_string(), instance_id.clone());
1154        }
1155
1156        if let Some(network_interface_id) = &request.network_interface_id {
1157            form_data.insert(
1158                "NetworkInterfaceId".to_string(),
1159                network_interface_id.clone(),
1160            );
1161        }
1162
1163        if let Some(vpc_peering_connection_id) = &request.vpc_peering_connection_id {
1164            form_data.insert(
1165                "VpcPeeringConnectionId".to_string(),
1166                vpc_peering_connection_id.clone(),
1167            );
1168        }
1169
1170        if let Some(transit_gateway_id) = &request.transit_gateway_id {
1171            form_data.insert("TransitGatewayId".to_string(), transit_gateway_id.clone());
1172        }
1173
1174        self.send_form_no_body(form_data, "CreateRoute", &request.route_table_id)
1175            .await
1176    }
1177
1178    async fn delete_route(&self, request: DeleteRouteRequest) -> Result<()> {
1179        let mut form_data = HashMap::new();
1180        form_data.insert("Action".to_string(), "DeleteRoute".to_string());
1181        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1182        form_data.insert("RouteTableId".to_string(), request.route_table_id.clone());
1183        form_data.insert(
1184            "DestinationCidrBlock".to_string(),
1185            request.destination_cidr_block.clone(),
1186        );
1187
1188        self.send_form_no_body(form_data, "DeleteRoute", &request.route_table_id)
1189            .await
1190    }
1191
1192    async fn associate_route_table(
1193        &self,
1194        request: AssociateRouteTableRequest,
1195    ) -> Result<AssociateRouteTableResponse> {
1196        let mut form_data = HashMap::new();
1197        form_data.insert("Action".to_string(), "AssociateRouteTable".to_string());
1198        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1199        form_data.insert("RouteTableId".to_string(), request.route_table_id.clone());
1200        form_data.insert("SubnetId".to_string(), request.subnet_id.clone());
1201
1202        self.send_form(form_data, "AssociateRouteTable", &request.route_table_id)
1203            .await
1204    }
1205
1206    async fn disassociate_route_table(&self, association_id: &str) -> Result<()> {
1207        let mut form_data = HashMap::new();
1208        form_data.insert("Action".to_string(), "DisassociateRouteTable".to_string());
1209        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1210        form_data.insert("AssociationId".to_string(), association_id.to_string());
1211
1212        self.send_form_no_body(form_data, "DisassociateRouteTable", association_id)
1213            .await
1214    }
1215
1216    // ---------------------------------------------------------------------------
1217    // Security Group Operations
1218    // ---------------------------------------------------------------------------
1219
1220    async fn describe_security_groups(
1221        &self,
1222        request: DescribeSecurityGroupsRequest,
1223    ) -> Result<DescribeSecurityGroupsResponse> {
1224        let mut form_data = HashMap::new();
1225        form_data.insert("Action".to_string(), "DescribeSecurityGroups".to_string());
1226        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1227
1228        if let Some(group_ids) = &request.group_ids {
1229            for (i, group_id) in group_ids.iter().enumerate() {
1230                form_data.insert(format!("GroupId.{}", i + 1), group_id.clone());
1231            }
1232        }
1233
1234        if let Some(group_names) = &request.group_names {
1235            for (i, group_name) in group_names.iter().enumerate() {
1236                form_data.insert(format!("GroupName.{}", i + 1), group_name.clone());
1237            }
1238        }
1239
1240        if let Some(filters) = &request.filters {
1241            Self::add_filters(&mut form_data, filters);
1242        }
1243
1244        if let Some(max_results) = request.max_results {
1245            form_data.insert("MaxResults".to_string(), max_results.to_string());
1246        }
1247
1248        if let Some(next_token) = &request.next_token {
1249            form_data.insert("NextToken".to_string(), next_token.clone());
1250        }
1251
1252        self.send_form(form_data, "DescribeSecurityGroups", "SecurityGroup")
1253            .await
1254    }
1255
1256    async fn describe_network_interfaces(
1257        &self,
1258        request: DescribeNetworkInterfacesRequest,
1259    ) -> Result<DescribeNetworkInterfacesResponse> {
1260        let mut form_data = HashMap::new();
1261        form_data.insert(
1262            "Action".to_string(),
1263            "DescribeNetworkInterfaces".to_string(),
1264        );
1265        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1266
1267        if let Some(network_interface_ids) = &request.network_interface_ids {
1268            for (i, network_interface_id) in network_interface_ids.iter().enumerate() {
1269                form_data.insert(
1270                    format!("NetworkInterfaceId.{}", i + 1),
1271                    network_interface_id.clone(),
1272                );
1273            }
1274        }
1275
1276        if let Some(filters) = &request.filters {
1277            Self::add_filters(&mut form_data, filters);
1278        }
1279
1280        if let Some(max_results) = request.max_results {
1281            form_data.insert("MaxResults".to_string(), max_results.to_string());
1282        }
1283
1284        if let Some(next_token) = &request.next_token {
1285            form_data.insert("NextToken".to_string(), next_token.clone());
1286        }
1287
1288        self.send_form(form_data, "DescribeNetworkInterfaces", "NetworkInterface")
1289            .await
1290    }
1291
1292    async fn create_security_group(
1293        &self,
1294        request: CreateSecurityGroupRequest,
1295    ) -> Result<CreateSecurityGroupResponse> {
1296        let mut form_data = HashMap::new();
1297        form_data.insert("Action".to_string(), "CreateSecurityGroup".to_string());
1298        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1299        form_data.insert("GroupName".to_string(), request.group_name.clone());
1300        form_data.insert("GroupDescription".to_string(), request.description.clone());
1301        form_data.insert("VpcId".to_string(), request.vpc_id.clone());
1302
1303        if let Some(tag_specs) = &request.tag_specifications {
1304            Self::add_tag_specifications(&mut form_data, tag_specs);
1305        }
1306
1307        self.send_form(form_data, "CreateSecurityGroup", &request.group_name)
1308            .await
1309    }
1310
1311    async fn delete_security_group(&self, group_id: &str) -> Result<()> {
1312        let mut form_data = HashMap::new();
1313        form_data.insert("Action".to_string(), "DeleteSecurityGroup".to_string());
1314        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1315        form_data.insert("GroupId".to_string(), group_id.to_string());
1316
1317        self.send_form_no_body(form_data, "DeleteSecurityGroup", group_id)
1318            .await
1319    }
1320
1321    async fn authorize_security_group_ingress(
1322        &self,
1323        request: AuthorizeSecurityGroupIngressRequest,
1324    ) -> Result<()> {
1325        let mut form_data = HashMap::new();
1326        form_data.insert(
1327            "Action".to_string(),
1328            "AuthorizeSecurityGroupIngress".to_string(),
1329        );
1330        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1331        form_data.insert("GroupId".to_string(), request.group_id.clone());
1332
1333        Self::add_ip_permissions(&mut form_data, &request.ip_permissions);
1334
1335        self.send_form_no_body(
1336            form_data,
1337            "AuthorizeSecurityGroupIngress",
1338            &request.group_id,
1339        )
1340        .await
1341    }
1342
1343    async fn authorize_security_group_egress(
1344        &self,
1345        request: AuthorizeSecurityGroupEgressRequest,
1346    ) -> Result<()> {
1347        let mut form_data = HashMap::new();
1348        form_data.insert(
1349            "Action".to_string(),
1350            "AuthorizeSecurityGroupEgress".to_string(),
1351        );
1352        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1353        form_data.insert("GroupId".to_string(), request.group_id.clone());
1354
1355        Self::add_ip_permissions(&mut form_data, &request.ip_permissions);
1356
1357        self.send_form_no_body(form_data, "AuthorizeSecurityGroupEgress", &request.group_id)
1358            .await
1359    }
1360
1361    async fn revoke_security_group_ingress(
1362        &self,
1363        request: RevokeSecurityGroupIngressRequest,
1364    ) -> Result<()> {
1365        let mut form_data = HashMap::new();
1366        form_data.insert(
1367            "Action".to_string(),
1368            "RevokeSecurityGroupIngress".to_string(),
1369        );
1370        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1371        form_data.insert("GroupId".to_string(), request.group_id.clone());
1372
1373        Self::add_ip_permissions(&mut form_data, &request.ip_permissions);
1374
1375        self.send_form_no_body(form_data, "RevokeSecurityGroupIngress", &request.group_id)
1376            .await
1377    }
1378
1379    async fn revoke_security_group_egress(
1380        &self,
1381        request: RevokeSecurityGroupEgressRequest,
1382    ) -> Result<()> {
1383        let mut form_data = HashMap::new();
1384        form_data.insert(
1385            "Action".to_string(),
1386            "RevokeSecurityGroupEgress".to_string(),
1387        );
1388        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1389        form_data.insert("GroupId".to_string(), request.group_id.clone());
1390
1391        Self::add_ip_permissions(&mut form_data, &request.ip_permissions);
1392
1393        self.send_form_no_body(form_data, "RevokeSecurityGroupEgress", &request.group_id)
1394            .await
1395    }
1396
1397    // ---------------------------------------------------------------------------
1398    // Availability Zone Operations
1399    // ---------------------------------------------------------------------------
1400
1401    async fn describe_availability_zones(
1402        &self,
1403        request: DescribeAvailabilityZonesRequest,
1404    ) -> Result<DescribeAvailabilityZonesResponse> {
1405        let mut form_data = HashMap::new();
1406        form_data.insert(
1407            "Action".to_string(),
1408            "DescribeAvailabilityZones".to_string(),
1409        );
1410        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1411
1412        if let Some(zone_names) = &request.zone_names {
1413            for (i, zone_name) in zone_names.iter().enumerate() {
1414                form_data.insert(format!("ZoneName.{}", i + 1), zone_name.clone());
1415            }
1416        }
1417
1418        if let Some(zone_ids) = &request.zone_ids {
1419            for (i, zone_id) in zone_ids.iter().enumerate() {
1420                form_data.insert(format!("ZoneId.{}", i + 1), zone_id.clone());
1421            }
1422        }
1423
1424        if let Some(filters) = &request.filters {
1425            Self::add_filters(&mut form_data, filters);
1426        }
1427
1428        if let Some(all_availability_zones) = request.all_availability_zones {
1429            form_data.insert(
1430                "AllAvailabilityZones".to_string(),
1431                all_availability_zones.to_string(),
1432            );
1433        }
1434
1435        self.send_form(form_data, "DescribeAvailabilityZones", "AvailabilityZone")
1436            .await
1437    }
1438
1439    // ---------------------------------------------------------------------------
1440    // AMI Operations
1441    // ---------------------------------------------------------------------------
1442
1443    async fn describe_images(
1444        &self,
1445        request: DescribeImagesRequest,
1446    ) -> Result<DescribeImagesResponse> {
1447        let mut form_data = HashMap::new();
1448        form_data.insert("Action".to_string(), "DescribeImages".to_string());
1449        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1450
1451        if let Some(image_ids) = &request.image_ids {
1452            for (i, image_id) in image_ids.iter().enumerate() {
1453                form_data.insert(format!("ImageId.{}", i + 1), image_id.clone());
1454            }
1455        }
1456
1457        if let Some(owners) = &request.owners {
1458            for (i, owner) in owners.iter().enumerate() {
1459                form_data.insert(format!("Owner.{}", i + 1), owner.clone());
1460            }
1461        }
1462
1463        if let Some(executable_users) = &request.executable_users {
1464            for (i, user) in executable_users.iter().enumerate() {
1465                form_data.insert(format!("ExecutableBy.{}", i + 1), user.clone());
1466            }
1467        }
1468
1469        if let Some(filters) = &request.filters {
1470            Self::add_filters(&mut form_data, filters);
1471        }
1472
1473        if let Some(include_deprecated) = request.include_deprecated {
1474            form_data.insert(
1475                "IncludeDeprecated".to_string(),
1476                include_deprecated.to_string(),
1477            );
1478        }
1479
1480        if let Some(max_results) = request.max_results {
1481            form_data.insert("MaxResults".to_string(), max_results.to_string());
1482        }
1483
1484        if let Some(next_token) = &request.next_token {
1485            form_data.insert("NextToken".to_string(), next_token.clone());
1486        }
1487
1488        self.send_form(form_data, "DescribeImages", "AMI").await
1489    }
1490
1491    // ---------------------------------------------------------------------------
1492    // Instance Operations
1493    // ---------------------------------------------------------------------------
1494
1495    async fn terminate_instances(
1496        &self,
1497        instance_ids: Vec<String>,
1498    ) -> Result<TerminateInstancesResponse> {
1499        let mut form_data = HashMap::new();
1500        form_data.insert("Action".to_string(), "TerminateInstances".to_string());
1501        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1502
1503        for (i, instance_id) in instance_ids.iter().enumerate() {
1504            form_data.insert(format!("InstanceId.{}", i + 1), instance_id.clone());
1505        }
1506
1507        let resource = instance_ids.join(",");
1508        self.send_form(form_data, "TerminateInstances", &resource)
1509            .await
1510    }
1511
1512    async fn describe_instances(
1513        &self,
1514        request: DescribeInstancesRequest,
1515    ) -> Result<DescribeInstancesResponse> {
1516        let mut form_data = HashMap::new();
1517        form_data.insert("Action".to_string(), "DescribeInstances".to_string());
1518        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1519
1520        if let Some(instance_ids) = &request.instance_ids {
1521            for (i, instance_id) in instance_ids.iter().enumerate() {
1522                form_data.insert(format!("InstanceId.{}", i + 1), instance_id.clone());
1523            }
1524        }
1525
1526        if let Some(filters) = &request.filters {
1527            Self::add_filters(&mut form_data, filters);
1528        }
1529
1530        if let Some(max_results) = request.max_results {
1531            form_data.insert("MaxResults".to_string(), max_results.to_string());
1532        }
1533
1534        if let Some(next_token) = &request.next_token {
1535            form_data.insert("NextToken".to_string(), next_token.clone());
1536        }
1537
1538        self.send_form(form_data, "DescribeInstances", "Instance")
1539            .await
1540    }
1541
1542    // ---------------------------------------------------------------------------
1543    // Volume Operations
1544    // ---------------------------------------------------------------------------
1545
1546    async fn create_volume(&self, request: CreateVolumeRequest) -> Result<CreateVolumeResponse> {
1547        let form_data = Self::create_volume_form_data(&request);
1548
1549        self.send_form(form_data, "CreateVolume", &request.availability_zone)
1550            .await
1551    }
1552
1553    async fn modify_volume(&self, request: ModifyVolumeRequest) -> Result<ModifyVolumeResponse> {
1554        let form_data = Self::modify_volume_form_data(&request);
1555        self.send_form(form_data, "ModifyVolume", &request.volume_id)
1556            .await
1557    }
1558
1559    async fn describe_volumes_modifications(
1560        &self,
1561        request: DescribeVolumesModificationsRequest,
1562    ) -> Result<DescribeVolumesModificationsResponse> {
1563        let form_data = Self::describe_volumes_modifications_form_data(&request);
1564        self.send_form(
1565            form_data,
1566            "DescribeVolumesModifications",
1567            "VolumeModification",
1568        )
1569        .await
1570    }
1571
1572    async fn delete_volume(&self, volume_id: &str) -> Result<()> {
1573        let mut form_data = HashMap::new();
1574        form_data.insert("Action".to_string(), "DeleteVolume".to_string());
1575        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1576        form_data.insert("VolumeId".to_string(), volume_id.to_string());
1577
1578        self.send_form_no_body(form_data, "DeleteVolume", volume_id)
1579            .await
1580    }
1581
1582    async fn describe_volumes(
1583        &self,
1584        request: DescribeVolumesRequest,
1585    ) -> Result<DescribeVolumesResponse> {
1586        let mut form_data = HashMap::new();
1587        form_data.insert("Action".to_string(), "DescribeVolumes".to_string());
1588        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1589
1590        if let Some(volume_ids) = &request.volume_ids {
1591            for (i, volume_id) in volume_ids.iter().enumerate() {
1592                form_data.insert(format!("VolumeId.{}", i + 1), volume_id.clone());
1593            }
1594        }
1595
1596        if let Some(filters) = &request.filters {
1597            Self::add_filters(&mut form_data, filters);
1598        }
1599
1600        if let Some(max_results) = request.max_results {
1601            form_data.insert("MaxResults".to_string(), max_results.to_string());
1602        }
1603
1604        if let Some(next_token) = &request.next_token {
1605            form_data.insert("NextToken".to_string(), next_token.clone());
1606        }
1607
1608        self.send_form(form_data, "DescribeVolumes", "Volume").await
1609    }
1610
1611    async fn attach_volume(&self, request: AttachVolumeRequest) -> Result<AttachVolumeResponse> {
1612        let mut form_data = HashMap::new();
1613        form_data.insert("Action".to_string(), "AttachVolume".to_string());
1614        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1615        form_data.insert("VolumeId".to_string(), request.volume_id.clone());
1616        form_data.insert("InstanceId".to_string(), request.instance_id.clone());
1617        form_data.insert("Device".to_string(), request.device.clone());
1618
1619        let resource = format!("{}:{}", request.volume_id, request.instance_id);
1620        self.send_form(form_data, "AttachVolume", &resource).await
1621    }
1622
1623    async fn detach_volume(&self, request: DetachVolumeRequest) -> Result<DetachVolumeResponse> {
1624        let mut form_data = HashMap::new();
1625        form_data.insert("Action".to_string(), "DetachVolume".to_string());
1626        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1627        form_data.insert("VolumeId".to_string(), request.volume_id.clone());
1628
1629        if let Some(instance_id) = &request.instance_id {
1630            form_data.insert("InstanceId".to_string(), instance_id.clone());
1631        }
1632
1633        if let Some(device) = &request.device {
1634            form_data.insert("Device".to_string(), device.clone());
1635        }
1636
1637        if let Some(force) = request.force {
1638            form_data.insert("Force".to_string(), force.to_string());
1639        }
1640
1641        self.send_form(form_data, "DetachVolume", &request.volume_id)
1642            .await
1643    }
1644
1645    // ---------------------------------------------------------------------------
1646    // Launch Template Operations
1647    // ---------------------------------------------------------------------------
1648
1649    async fn create_launch_template(
1650        &self,
1651        request: CreateLaunchTemplateRequest,
1652    ) -> Result<CreateLaunchTemplateResponse> {
1653        let mut form_data = HashMap::new();
1654        form_data.insert("Action".to_string(), "CreateLaunchTemplate".to_string());
1655        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1656        form_data.insert(
1657            "LaunchTemplateName".to_string(),
1658            request.launch_template_name.clone(),
1659        );
1660
1661        if let Some(version_description) = &request.version_description {
1662            form_data.insert(
1663                "VersionDescription".to_string(),
1664                version_description.clone(),
1665            );
1666        }
1667
1668        // Add launch template data
1669        let data = &request.launch_template_data;
1670
1671        if let Some(image_id) = &data.image_id {
1672            form_data.insert("LaunchTemplateData.ImageId".to_string(), image_id.clone());
1673        }
1674
1675        if let Some(instance_type) = &data.instance_type {
1676            form_data.insert(
1677                "LaunchTemplateData.InstanceType".to_string(),
1678                instance_type.clone(),
1679            );
1680        }
1681
1682        if let Some(key_name) = &data.key_name {
1683            form_data.insert("LaunchTemplateData.KeyName".to_string(), key_name.clone());
1684        }
1685
1686        if let Some(user_data) = &data.user_data {
1687            form_data.insert("LaunchTemplateData.UserData".to_string(), user_data.clone());
1688        }
1689
1690        if let Some(security_group_ids) = &data.security_group_ids {
1691            for (i, sg_id) in security_group_ids.iter().enumerate() {
1692                form_data.insert(
1693                    format!("LaunchTemplateData.SecurityGroupId.{}", i + 1),
1694                    sg_id.clone(),
1695                );
1696            }
1697        }
1698
1699        if let Some(iam_instance_profile) = &data.iam_instance_profile {
1700            if let Some(arn) = &iam_instance_profile.arn {
1701                form_data.insert(
1702                    "LaunchTemplateData.IamInstanceProfile.Arn".to_string(),
1703                    arn.clone(),
1704                );
1705            }
1706            if let Some(name) = &iam_instance_profile.name {
1707                form_data.insert(
1708                    "LaunchTemplateData.IamInstanceProfile.Name".to_string(),
1709                    name.clone(),
1710                );
1711            }
1712        }
1713
1714        if let Some(block_device_mappings) = &data.block_device_mappings {
1715            for (i, bdm) in block_device_mappings.iter().enumerate() {
1716                let idx = i + 1;
1717                if let Some(device_name) = &bdm.device_name {
1718                    form_data.insert(
1719                        format!("LaunchTemplateData.BlockDeviceMapping.{}.DeviceName", idx),
1720                        device_name.clone(),
1721                    );
1722                }
1723                if let Some(ebs) = &bdm.ebs {
1724                    if let Some(volume_size) = ebs.volume_size {
1725                        form_data.insert(
1726                            format!(
1727                                "LaunchTemplateData.BlockDeviceMapping.{}.Ebs.VolumeSize",
1728                                idx
1729                            ),
1730                            volume_size.to_string(),
1731                        );
1732                    }
1733                    if let Some(volume_type) = &ebs.volume_type {
1734                        form_data.insert(
1735                            format!(
1736                                "LaunchTemplateData.BlockDeviceMapping.{}.Ebs.VolumeType",
1737                                idx
1738                            ),
1739                            volume_type.clone(),
1740                        );
1741                    }
1742                    if let Some(delete_on_termination) = ebs.delete_on_termination {
1743                        form_data.insert(
1744                            format!(
1745                                "LaunchTemplateData.BlockDeviceMapping.{}.Ebs.DeleteOnTermination",
1746                                idx
1747                            ),
1748                            delete_on_termination.to_string(),
1749                        );
1750                    }
1751                    if let Some(encrypted) = ebs.encrypted {
1752                        form_data.insert(
1753                            format!(
1754                                "LaunchTemplateData.BlockDeviceMapping.{}.Ebs.Encrypted",
1755                                idx
1756                            ),
1757                            encrypted.to_string(),
1758                        );
1759                    }
1760                    if let Some(iops) = ebs.iops {
1761                        form_data.insert(
1762                            format!("LaunchTemplateData.BlockDeviceMapping.{}.Ebs.Iops", idx),
1763                            iops.to_string(),
1764                        );
1765                    }
1766                    if let Some(throughput) = ebs.throughput {
1767                        form_data.insert(
1768                            format!(
1769                                "LaunchTemplateData.BlockDeviceMapping.{}.Ebs.Throughput",
1770                                idx
1771                            ),
1772                            throughput.to_string(),
1773                        );
1774                    }
1775                }
1776            }
1777        }
1778
1779        if let Some(network_interfaces) = &data.network_interfaces {
1780            for (i, ni) in network_interfaces.iter().enumerate() {
1781                let idx = i + 1;
1782                if let Some(device_index) = ni.device_index {
1783                    form_data.insert(
1784                        format!("LaunchTemplateData.NetworkInterface.{}.DeviceIndex", idx),
1785                        device_index.to_string(),
1786                    );
1787                }
1788                if let Some(associate_public_ip) = ni.associate_public_ip_address {
1789                    form_data.insert(
1790                        format!(
1791                            "LaunchTemplateData.NetworkInterface.{}.AssociatePublicIpAddress",
1792                            idx
1793                        ),
1794                        associate_public_ip.to_string(),
1795                    );
1796                }
1797                if let Some(subnet_id) = &ni.subnet_id {
1798                    form_data.insert(
1799                        format!("LaunchTemplateData.NetworkInterface.{}.SubnetId", idx),
1800                        subnet_id.clone(),
1801                    );
1802                }
1803                if let Some(groups) = &ni.groups {
1804                    for (j, group) in groups.iter().enumerate() {
1805                        form_data.insert(
1806                            format!(
1807                                "LaunchTemplateData.NetworkInterface.{}.SecurityGroupId.{}",
1808                                idx,
1809                                j + 1
1810                            ),
1811                            group.clone(),
1812                        );
1813                    }
1814                }
1815            }
1816        }
1817
1818        if let Some(metadata_options) = &data.metadata_options {
1819            if let Some(http_tokens) = &metadata_options.http_tokens {
1820                form_data.insert(
1821                    "LaunchTemplateData.MetadataOptions.HttpTokens".to_string(),
1822                    http_tokens.clone(),
1823                );
1824            }
1825            if let Some(http_endpoint) = &metadata_options.http_endpoint {
1826                form_data.insert(
1827                    "LaunchTemplateData.MetadataOptions.HttpEndpoint".to_string(),
1828                    http_endpoint.clone(),
1829                );
1830            }
1831            if let Some(http_put_response_hop_limit) = metadata_options.http_put_response_hop_limit
1832            {
1833                form_data.insert(
1834                    "LaunchTemplateData.MetadataOptions.HttpPutResponseHopLimit".to_string(),
1835                    http_put_response_hop_limit.to_string(),
1836                );
1837            }
1838            if let Some(instance_metadata_tags) = &metadata_options.instance_metadata_tags {
1839                form_data.insert(
1840                    "LaunchTemplateData.MetadataOptions.InstanceMetadataTags".to_string(),
1841                    instance_metadata_tags.clone(),
1842                );
1843            }
1844        }
1845
1846        Self::add_cpu_options(&mut form_data, data.cpu_options.as_ref());
1847
1848        if let Some(tag_specs) = &data.tag_specifications {
1849            Self::add_tag_specifications_with_prefix(
1850                &mut form_data,
1851                "LaunchTemplateData.TagSpecification",
1852                tag_specs,
1853            );
1854        }
1855
1856        if let Some(tag_specs) = &request.tag_specifications {
1857            Self::add_tag_specifications(&mut form_data, tag_specs);
1858        }
1859
1860        self.send_form(
1861            form_data,
1862            "CreateLaunchTemplate",
1863            &request.launch_template_name,
1864        )
1865        .await
1866    }
1867
1868    async fn create_launch_template_version(
1869        &self,
1870        request: CreateLaunchTemplateVersionRequest,
1871    ) -> Result<CreateLaunchTemplateVersionResponse> {
1872        let mut form_data = HashMap::new();
1873        form_data.insert(
1874            "Action".to_string(),
1875            "CreateLaunchTemplateVersion".to_string(),
1876        );
1877        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1878
1879        let resource_name;
1880        if let Some(ref id) = request.launch_template_id {
1881            form_data.insert("LaunchTemplateId".to_string(), id.clone());
1882            resource_name = id.clone();
1883        } else if let Some(ref name) = request.launch_template_name {
1884            form_data.insert("LaunchTemplateName".to_string(), name.clone());
1885            resource_name = name.clone();
1886        } else {
1887            return Err(alien_error::AlienError::new(ErrorData::InvalidInput {
1888                message: "Either launch_template_id or launch_template_name must be provided"
1889                    .to_string(),
1890                field_name: Some("launch_template_id".to_string()),
1891            }));
1892        }
1893
1894        if let Some(ref source_version) = request.source_version {
1895            form_data.insert("SourceVersion".to_string(), source_version.clone());
1896        }
1897        if let Some(ref description) = request.version_description {
1898            form_data.insert("VersionDescription".to_string(), description.clone());
1899        }
1900
1901        let data = &request.launch_template_data;
1902        if let Some(ref user_data) = data.user_data {
1903            form_data.insert("LaunchTemplateData.UserData".to_string(), user_data.clone());
1904        }
1905        if let Some(ref image_id) = data.image_id {
1906            form_data.insert("LaunchTemplateData.ImageId".to_string(), image_id.clone());
1907        }
1908        if let Some(ref instance_type) = data.instance_type {
1909            form_data.insert(
1910                "LaunchTemplateData.InstanceType".to_string(),
1911                instance_type.clone(),
1912            );
1913        }
1914        if let Some(metadata_options) = &data.metadata_options {
1915            if let Some(http_tokens) = &metadata_options.http_tokens {
1916                form_data.insert(
1917                    "LaunchTemplateData.MetadataOptions.HttpTokens".to_string(),
1918                    http_tokens.clone(),
1919                );
1920            }
1921            if let Some(http_endpoint) = &metadata_options.http_endpoint {
1922                form_data.insert(
1923                    "LaunchTemplateData.MetadataOptions.HttpEndpoint".to_string(),
1924                    http_endpoint.clone(),
1925                );
1926            }
1927            if let Some(http_put_response_hop_limit) = metadata_options.http_put_response_hop_limit
1928            {
1929                form_data.insert(
1930                    "LaunchTemplateData.MetadataOptions.HttpPutResponseHopLimit".to_string(),
1931                    http_put_response_hop_limit.to_string(),
1932                );
1933            }
1934            if let Some(instance_metadata_tags) = &metadata_options.instance_metadata_tags {
1935                form_data.insert(
1936                    "LaunchTemplateData.MetadataOptions.InstanceMetadataTags".to_string(),
1937                    instance_metadata_tags.clone(),
1938                );
1939            }
1940        }
1941        Self::add_cpu_options(&mut form_data, data.cpu_options.as_ref());
1942        if let Some(tag_specs) = &data.tag_specifications {
1943            Self::add_tag_specifications_with_prefix(
1944                &mut form_data,
1945                "LaunchTemplateData.TagSpecification",
1946                tag_specs,
1947            );
1948        }
1949
1950        self.send_form(form_data, "CreateLaunchTemplateVersion", &resource_name)
1951            .await
1952    }
1953
1954    async fn delete_launch_template(
1955        &self,
1956        request: DeleteLaunchTemplateRequest,
1957    ) -> Result<DeleteLaunchTemplateResponse> {
1958        let mut form_data = HashMap::new();
1959        form_data.insert("Action".to_string(), "DeleteLaunchTemplate".to_string());
1960        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1961
1962        let resource: String;
1963        if let Some(launch_template_id) = &request.launch_template_id {
1964            form_data.insert("LaunchTemplateId".to_string(), launch_template_id.clone());
1965            resource = launch_template_id.clone();
1966        } else if let Some(launch_template_name) = &request.launch_template_name {
1967            form_data.insert(
1968                "LaunchTemplateName".to_string(),
1969                launch_template_name.clone(),
1970            );
1971            resource = launch_template_name.clone();
1972        } else {
1973            resource = "unknown".to_string();
1974        }
1975
1976        self.send_form(form_data, "DeleteLaunchTemplate", &resource)
1977            .await
1978    }
1979
1980    async fn describe_launch_templates(
1981        &self,
1982        request: DescribeLaunchTemplatesRequest,
1983    ) -> Result<DescribeLaunchTemplatesResponse> {
1984        let mut form_data = HashMap::new();
1985        form_data.insert("Action".to_string(), "DescribeLaunchTemplates".to_string());
1986        form_data.insert("Version".to_string(), "2016-11-15".to_string());
1987
1988        if let Some(launch_template_ids) = &request.launch_template_ids {
1989            for (i, lt_id) in launch_template_ids.iter().enumerate() {
1990                form_data.insert(format!("LaunchTemplateId.{}", i + 1), lt_id.clone());
1991            }
1992        }
1993
1994        if let Some(launch_template_names) = &request.launch_template_names {
1995            for (i, lt_name) in launch_template_names.iter().enumerate() {
1996                form_data.insert(format!("LaunchTemplateName.{}", i + 1), lt_name.clone());
1997            }
1998        }
1999
2000        if let Some(filters) = &request.filters {
2001            Self::add_filters(&mut form_data, filters);
2002        }
2003
2004        if let Some(max_results) = request.max_results {
2005            form_data.insert("MaxResults".to_string(), max_results.to_string());
2006        }
2007
2008        if let Some(next_token) = &request.next_token {
2009            form_data.insert("NextToken".to_string(), next_token.clone());
2010        }
2011
2012        self.send_form(form_data, "DescribeLaunchTemplates", "LaunchTemplate")
2013            .await
2014    }
2015
2016    async fn get_console_output(&self, instance_id: String) -> Result<GetConsoleOutputResponse> {
2017        let mut form_data = HashMap::new();
2018        form_data.insert("Action".to_string(), "GetConsoleOutput".to_string());
2019        form_data.insert("Version".to_string(), "2016-11-15".to_string());
2020        form_data.insert("InstanceId".to_string(), instance_id.clone());
2021
2022        self.send_form(form_data, "GetConsoleOutput", &instance_id)
2023            .await
2024    }
2025}
2026
2027impl Ec2Client {
2028    /// Append `LaunchTemplateData.CpuOptions.*` form fields.
2029    ///
2030    /// Currently only emits `NestedVirtualization` when set. AWS validates
2031    /// support at launch time and rejects unsupported instance types with a
2032    /// clear error (nested virt is only available on 8th-gen Intel: c8i/m8i/r8i
2033    /// and their flex variants), so no client-side allowlist is needed.
2034    fn add_cpu_options(
2035        form_data: &mut HashMap<String, String>,
2036        cpu_options: Option<&LaunchTemplateCpuOptions>,
2037    ) {
2038        let Some(cpu_options) = cpu_options else {
2039            return;
2040        };
2041        if let Some(nested) = &cpu_options.nested_virtualization {
2042            form_data.insert(
2043                "LaunchTemplateData.CpuOptions.NestedVirtualization".to_string(),
2044                nested.clone(),
2045            );
2046        }
2047    }
2048
2049    /// Add IP permissions to form data for security group operations.
2050    fn add_ip_permissions(form_data: &mut HashMap<String, String>, permissions: &[IpPermission]) {
2051        for (i, perm) in permissions.iter().enumerate() {
2052            let idx = i + 1;
2053            form_data.insert(
2054                format!("IpPermissions.{}.IpProtocol", idx),
2055                perm.ip_protocol.clone(),
2056            );
2057
2058            if let Some(from_port) = perm.from_port {
2059                form_data.insert(
2060                    format!("IpPermissions.{}.FromPort", idx),
2061                    from_port.to_string(),
2062                );
2063            }
2064
2065            if let Some(to_port) = perm.to_port {
2066                form_data.insert(format!("IpPermissions.{}.ToPort", idx), to_port.to_string());
2067            }
2068
2069            if let Some(ip_ranges) = &perm.ip_ranges {
2070                for (j, range) in ip_ranges.iter().enumerate() {
2071                    form_data.insert(
2072                        format!("IpPermissions.{}.IpRanges.{}.CidrIp", idx, j + 1),
2073                        range.cidr_ip.clone(),
2074                    );
2075                    if let Some(description) = &range.description {
2076                        form_data.insert(
2077                            format!("IpPermissions.{}.IpRanges.{}.Description", idx, j + 1),
2078                            description.clone(),
2079                        );
2080                    }
2081                }
2082            }
2083
2084            if let Some(ipv6_ranges) = &perm.ipv6_ranges {
2085                for (j, range) in ipv6_ranges.iter().enumerate() {
2086                    form_data.insert(
2087                        format!("IpPermissions.{}.Ipv6Ranges.{}.CidrIpv6", idx, j + 1),
2088                        range.cidr_ipv6.clone(),
2089                    );
2090                    if let Some(description) = &range.description {
2091                        form_data.insert(
2092                            format!("IpPermissions.{}.Ipv6Ranges.{}.Description", idx, j + 1),
2093                            description.clone(),
2094                        );
2095                    }
2096                }
2097            }
2098
2099            if let Some(user_id_group_pairs) = &perm.user_id_group_pairs {
2100                for (j, pair) in user_id_group_pairs.iter().enumerate() {
2101                    if let Some(group_id) = &pair.group_id {
2102                        form_data.insert(
2103                            format!("IpPermissions.{}.Groups.{}.GroupId", idx, j + 1),
2104                            group_id.clone(),
2105                        );
2106                    }
2107                    if let Some(user_id) = &pair.user_id {
2108                        form_data.insert(
2109                            format!("IpPermissions.{}.Groups.{}.UserId", idx, j + 1),
2110                            user_id.clone(),
2111                        );
2112                    }
2113                    if let Some(description) = &pair.description {
2114                        form_data.insert(
2115                            format!("IpPermissions.{}.Groups.{}.Description", idx, j + 1),
2116                            description.clone(),
2117                        );
2118                    }
2119                }
2120            }
2121        }
2122    }
2123}
2124
2125// ---------------------------------------------------------------------------
2126// Common Types
2127// ---------------------------------------------------------------------------
2128
2129/// A filter to apply when describing resources.
2130#[derive(Debug, Clone, Serialize, Builder)]
2131pub struct Filter {
2132    /// The name of the filter.
2133    pub name: String,
2134    /// The filter values.
2135    pub values: Vec<String>,
2136}
2137
2138/// A tag to apply to a resource.
2139/// Note: EC2 XML responses use lowercase `<key>` and `<value>` tags.
2140#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
2141pub struct Tag {
2142    /// The key of the tag.
2143    pub key: String,
2144    /// The value of the tag.
2145    pub value: String,
2146}
2147
2148/// Tag specification for creating resources with tags.
2149#[derive(Debug, Clone, Serialize, Builder)]
2150pub struct TagSpecification {
2151    /// The type of resource to tag.
2152    pub resource_type: String,
2153    /// The tags to apply.
2154    pub tags: Vec<Tag>,
2155}
2156
2157// ---------------------------------------------------------------------------
2158// VPC Request/Response Types
2159// ---------------------------------------------------------------------------
2160
2161/// Request to describe VPCs.
2162#[derive(Debug, Clone, Serialize, Builder, Default)]
2163pub struct DescribeVpcsRequest {
2164    #[serde(skip_serializing_if = "Option::is_none")]
2165    pub vpc_ids: Option<Vec<String>>,
2166    #[serde(skip_serializing_if = "Option::is_none")]
2167    pub filters: Option<Vec<Filter>>,
2168    #[serde(skip_serializing_if = "Option::is_none")]
2169    pub max_results: Option<i32>,
2170    #[serde(skip_serializing_if = "Option::is_none")]
2171    pub next_token: Option<String>,
2172}
2173
2174/// Response from describing VPCs.
2175#[derive(Debug, Deserialize)]
2176#[serde(rename_all = "camelCase")]
2177pub struct DescribeVpcsResponse {
2178    #[serde(rename = "vpcSet")]
2179    pub vpc_set: Option<VpcSet>,
2180    #[serde(rename = "nextToken")]
2181    pub next_token: Option<String>,
2182}
2183
2184#[derive(Debug, Deserialize)]
2185#[serde(rename_all = "camelCase")]
2186pub struct VpcSet {
2187    #[serde(rename = "item", default)]
2188    pub items: Vec<Vpc>,
2189}
2190
2191/// Represents a VPC.
2192#[derive(Debug, Clone, Deserialize)]
2193#[serde(rename_all = "camelCase")]
2194pub struct Vpc {
2195    pub vpc_id: Option<String>,
2196    pub state: Option<String>,
2197    pub cidr_block: Option<String>,
2198    pub owner_id: Option<String>,
2199    pub instance_tenancy: Option<String>,
2200    pub is_default: Option<bool>,
2201    #[serde(rename = "tagSet")]
2202    pub tag_set: Option<TagSet>,
2203}
2204
2205#[derive(Debug, Clone, Deserialize)]
2206#[serde(rename_all = "camelCase")]
2207pub struct TagSet {
2208    #[serde(rename = "item", default)]
2209    pub items: Vec<Tag>,
2210}
2211
2212/// Request to describe a VPC attribute.
2213#[derive(Debug, Clone, Serialize, Builder)]
2214pub struct DescribeVpcAttributeRequest {
2215    pub vpc_id: String,
2216    /// The attribute to describe: "enableDnsSupport" or "enableDnsHostnames"
2217    pub attribute: String,
2218}
2219
2220/// Response from describing a VPC attribute.
2221#[derive(Debug, Deserialize)]
2222#[serde(rename_all = "camelCase")]
2223pub struct DescribeVpcAttributeResponse {
2224    pub vpc_id: Option<String>,
2225    #[serde(rename = "enableDnsSupport")]
2226    pub enable_dns_support: Option<AttributeBooleanValue>,
2227    #[serde(rename = "enableDnsHostnames")]
2228    pub enable_dns_hostnames: Option<AttributeBooleanValue>,
2229}
2230
2231#[derive(Debug, Deserialize)]
2232#[serde(rename_all = "camelCase")]
2233pub struct AttributeBooleanValue {
2234    pub value: Option<bool>,
2235}
2236
2237/// Request to create a VPC.
2238#[derive(Debug, Clone, Serialize, Builder)]
2239pub struct CreateVpcRequest {
2240    pub cidr_block: String,
2241    #[serde(skip_serializing_if = "Option::is_none")]
2242    pub instance_tenancy: Option<String>,
2243    #[serde(skip_serializing_if = "Option::is_none")]
2244    pub amazon_provided_ipv6_cidr_block: Option<bool>,
2245    #[serde(skip_serializing_if = "Option::is_none")]
2246    pub tag_specifications: Option<Vec<TagSpecification>>,
2247}
2248
2249/// Response from creating a VPC.
2250#[derive(Debug, Deserialize)]
2251#[serde(rename_all = "camelCase")]
2252pub struct CreateVpcResponse {
2253    pub vpc: Option<Vpc>,
2254}
2255
2256/// Request to modify VPC attributes.
2257#[derive(Debug, Clone, Serialize, Builder)]
2258pub struct ModifyVpcAttributeRequest {
2259    pub vpc_id: String,
2260    #[serde(skip_serializing_if = "Option::is_none")]
2261    pub enable_dns_support: Option<bool>,
2262    #[serde(skip_serializing_if = "Option::is_none")]
2263    pub enable_dns_hostnames: Option<bool>,
2264}
2265
2266// ---------------------------------------------------------------------------
2267// Subnet Request/Response Types
2268// ---------------------------------------------------------------------------
2269
2270/// Request to describe subnets.
2271#[derive(Debug, Clone, Serialize, Builder, Default)]
2272pub struct DescribeSubnetsRequest {
2273    #[serde(skip_serializing_if = "Option::is_none")]
2274    pub subnet_ids: Option<Vec<String>>,
2275    #[serde(skip_serializing_if = "Option::is_none")]
2276    pub filters: Option<Vec<Filter>>,
2277    #[serde(skip_serializing_if = "Option::is_none")]
2278    pub max_results: Option<i32>,
2279    #[serde(skip_serializing_if = "Option::is_none")]
2280    pub next_token: Option<String>,
2281}
2282
2283/// Response from describing subnets.
2284#[derive(Debug, Deserialize)]
2285#[serde(rename_all = "camelCase")]
2286pub struct DescribeSubnetsResponse {
2287    #[serde(rename = "subnetSet")]
2288    pub subnet_set: Option<SubnetSet>,
2289    #[serde(rename = "nextToken")]
2290    pub next_token: Option<String>,
2291}
2292
2293#[derive(Debug, Deserialize)]
2294#[serde(rename_all = "camelCase")]
2295pub struct SubnetSet {
2296    #[serde(rename = "item", default)]
2297    pub items: Vec<Subnet>,
2298}
2299
2300/// Represents a subnet.
2301#[derive(Debug, Clone, Deserialize)]
2302#[serde(rename_all = "camelCase")]
2303pub struct Subnet {
2304    pub subnet_id: Option<String>,
2305    pub vpc_id: Option<String>,
2306    pub state: Option<String>,
2307    pub cidr_block: Option<String>,
2308    pub availability_zone: Option<String>,
2309    pub availability_zone_id: Option<String>,
2310    pub available_ip_address_count: Option<i32>,
2311    pub default_for_az: Option<bool>,
2312    pub map_public_ip_on_launch: Option<bool>,
2313    #[serde(rename = "tagSet")]
2314    pub tag_set: Option<TagSet>,
2315}
2316
2317/// Request to create a subnet.
2318#[derive(Debug, Clone, Serialize, Builder)]
2319pub struct CreateSubnetRequest {
2320    pub vpc_id: String,
2321    pub cidr_block: String,
2322    #[serde(skip_serializing_if = "Option::is_none")]
2323    pub availability_zone: Option<String>,
2324    #[serde(skip_serializing_if = "Option::is_none")]
2325    pub availability_zone_id: Option<String>,
2326    #[serde(skip_serializing_if = "Option::is_none")]
2327    pub tag_specifications: Option<Vec<TagSpecification>>,
2328}
2329
2330/// Response from creating a subnet.
2331#[derive(Debug, Deserialize)]
2332#[serde(rename_all = "camelCase")]
2333pub struct CreateSubnetResponse {
2334    pub subnet: Option<Subnet>,
2335}
2336
2337// ---------------------------------------------------------------------------
2338// Internet Gateway Request/Response Types
2339// ---------------------------------------------------------------------------
2340
2341/// Request to create an internet gateway.
2342#[derive(Debug, Clone, Serialize, Builder, Default)]
2343pub struct CreateInternetGatewayRequest {
2344    #[serde(skip_serializing_if = "Option::is_none")]
2345    pub tag_specifications: Option<Vec<TagSpecification>>,
2346}
2347
2348/// Response from creating an internet gateway.
2349#[derive(Debug, Deserialize)]
2350#[serde(rename_all = "camelCase")]
2351pub struct CreateInternetGatewayResponse {
2352    #[serde(rename = "internetGateway")]
2353    pub internet_gateway: Option<InternetGateway>,
2354}
2355
2356/// Represents an internet gateway.
2357#[derive(Debug, Clone, Deserialize)]
2358#[serde(rename_all = "camelCase")]
2359pub struct InternetGateway {
2360    pub internet_gateway_id: Option<String>,
2361    #[serde(rename = "attachmentSet")]
2362    pub attachment_set: Option<InternetGatewayAttachmentSet>,
2363    #[serde(rename = "tagSet")]
2364    pub tag_set: Option<TagSet>,
2365}
2366
2367#[derive(Debug, Clone, Deserialize)]
2368#[serde(rename_all = "camelCase")]
2369pub struct InternetGatewayAttachmentSet {
2370    #[serde(rename = "item", default)]
2371    pub items: Vec<InternetGatewayAttachment>,
2372}
2373
2374#[derive(Debug, Clone, Deserialize)]
2375#[serde(rename_all = "camelCase")]
2376pub struct InternetGatewayAttachment {
2377    pub vpc_id: Option<String>,
2378    pub state: Option<String>,
2379}
2380
2381/// Request to attach an internet gateway to a VPC.
2382#[derive(Debug, Clone, Serialize, Builder)]
2383pub struct AttachInternetGatewayRequest {
2384    pub internet_gateway_id: String,
2385    pub vpc_id: String,
2386}
2387
2388/// Request to detach an internet gateway from a VPC.
2389#[derive(Debug, Clone, Serialize, Builder)]
2390pub struct DetachInternetGatewayRequest {
2391    pub internet_gateway_id: String,
2392    pub vpc_id: String,
2393}
2394
2395/// Request to describe internet gateways.
2396#[derive(Debug, Clone, Serialize, Builder, Default)]
2397pub struct DescribeInternetGatewaysRequest {
2398    #[serde(skip_serializing_if = "Option::is_none")]
2399    pub internet_gateway_ids: Option<Vec<String>>,
2400    #[serde(skip_serializing_if = "Option::is_none")]
2401    pub filters: Option<Vec<Filter>>,
2402    #[serde(skip_serializing_if = "Option::is_none")]
2403    pub max_results: Option<i32>,
2404    #[serde(skip_serializing_if = "Option::is_none")]
2405    pub next_token: Option<String>,
2406}
2407
2408/// Response from describing internet gateways.
2409#[derive(Debug, Deserialize)]
2410#[serde(rename_all = "camelCase")]
2411pub struct DescribeInternetGatewaysResponse {
2412    #[serde(rename = "internetGatewaySet")]
2413    pub internet_gateway_set: Option<InternetGatewaySet>,
2414    #[serde(rename = "nextToken")]
2415    pub next_token: Option<String>,
2416}
2417
2418#[derive(Debug, Deserialize)]
2419#[serde(rename_all = "camelCase")]
2420pub struct InternetGatewaySet {
2421    #[serde(rename = "item", default)]
2422    pub items: Vec<InternetGateway>,
2423}
2424
2425// ---------------------------------------------------------------------------
2426// NAT Gateway Request/Response Types
2427// ---------------------------------------------------------------------------
2428
2429/// Request to create a NAT gateway.
2430#[derive(Debug, Clone, Serialize, Builder)]
2431pub struct CreateNatGatewayRequest {
2432    /// The subnet in which to create the NAT gateway.
2433    pub subnet_id: String,
2434    /// [Public NAT only] The allocation ID of the Elastic IP address for the gateway.
2435    #[serde(skip_serializing_if = "Option::is_none")]
2436    pub allocation_id: Option<String>,
2437    /// The connectivity type: "public" (default) or "private".
2438    #[serde(skip_serializing_if = "Option::is_none")]
2439    pub connectivity_type: Option<String>,
2440    /// The private IPv4 address to assign to the NAT gateway.
2441    #[serde(skip_serializing_if = "Option::is_none")]
2442    pub private_ip_address: Option<String>,
2443    #[serde(skip_serializing_if = "Option::is_none")]
2444    pub tag_specifications: Option<Vec<TagSpecification>>,
2445}
2446
2447/// Response from creating a NAT gateway.
2448#[derive(Debug, Deserialize)]
2449#[serde(rename_all = "camelCase")]
2450pub struct CreateNatGatewayResponse {
2451    #[serde(rename = "natGateway")]
2452    pub nat_gateway: Option<NatGateway>,
2453}
2454
2455/// Represents a NAT gateway.
2456#[derive(Debug, Clone, Deserialize)]
2457#[serde(rename_all = "camelCase")]
2458pub struct NatGateway {
2459    pub nat_gateway_id: Option<String>,
2460    pub subnet_id: Option<String>,
2461    pub vpc_id: Option<String>,
2462    pub state: Option<String>,
2463    pub connectivity_type: Option<String>,
2464    #[serde(rename = "natGatewayAddressSet")]
2465    pub nat_gateway_address_set: Option<NatGatewayAddressSet>,
2466    #[serde(rename = "tagSet")]
2467    pub tag_set: Option<TagSet>,
2468}
2469
2470#[derive(Debug, Clone, Deserialize)]
2471#[serde(rename_all = "camelCase")]
2472pub struct NatGatewayAddressSet {
2473    #[serde(rename = "item", default)]
2474    pub items: Vec<NatGatewayAddress>,
2475}
2476
2477#[derive(Debug, Clone, Deserialize)]
2478#[serde(rename_all = "camelCase")]
2479pub struct NatGatewayAddress {
2480    pub allocation_id: Option<String>,
2481    pub network_interface_id: Option<String>,
2482    pub private_ip: Option<String>,
2483    pub public_ip: Option<String>,
2484}
2485
2486/// Response from deleting a NAT gateway.
2487#[derive(Debug, Deserialize)]
2488#[serde(rename_all = "camelCase")]
2489pub struct DeleteNatGatewayResponse {
2490    pub nat_gateway_id: Option<String>,
2491}
2492
2493/// Request to describe NAT gateways.
2494#[derive(Debug, Clone, Serialize, Builder, Default)]
2495pub struct DescribeNatGatewaysRequest {
2496    #[serde(skip_serializing_if = "Option::is_none")]
2497    pub nat_gateway_ids: Option<Vec<String>>,
2498    #[serde(skip_serializing_if = "Option::is_none")]
2499    pub filters: Option<Vec<Filter>>,
2500    #[serde(skip_serializing_if = "Option::is_none")]
2501    pub max_results: Option<i32>,
2502    #[serde(skip_serializing_if = "Option::is_none")]
2503    pub next_token: Option<String>,
2504}
2505
2506/// Response from describing NAT gateways.
2507#[derive(Debug, Deserialize)]
2508#[serde(rename_all = "camelCase")]
2509pub struct DescribeNatGatewaysResponse {
2510    #[serde(rename = "natGatewaySet")]
2511    pub nat_gateway_set: Option<NatGatewaySet>,
2512    #[serde(rename = "nextToken")]
2513    pub next_token: Option<String>,
2514}
2515
2516#[derive(Debug, Deserialize)]
2517#[serde(rename_all = "camelCase")]
2518pub struct NatGatewaySet {
2519    #[serde(rename = "item", default)]
2520    pub items: Vec<NatGateway>,
2521}
2522
2523// ---------------------------------------------------------------------------
2524// Elastic IP Request/Response Types
2525// ---------------------------------------------------------------------------
2526
2527/// Request to allocate an Elastic IP address.
2528#[derive(Debug, Clone, Serialize, Builder, Default)]
2529pub struct AllocateAddressRequest {
2530    /// The domain: "vpc" (default) or "standard".
2531    #[serde(skip_serializing_if = "Option::is_none")]
2532    pub domain: Option<String>,
2533    #[serde(skip_serializing_if = "Option::is_none")]
2534    pub tag_specifications: Option<Vec<TagSpecification>>,
2535}
2536
2537/// Response from allocating an Elastic IP address.
2538#[derive(Debug, Deserialize)]
2539#[serde(rename_all = "camelCase")]
2540pub struct AllocateAddressResponse {
2541    pub public_ip: Option<String>,
2542    pub allocation_id: Option<String>,
2543    pub domain: Option<String>,
2544}
2545
2546// ---------------------------------------------------------------------------
2547// Route Table Request/Response Types
2548// ---------------------------------------------------------------------------
2549
2550/// Request to describe route tables.
2551#[derive(Debug, Clone, Serialize, Builder, Default)]
2552pub struct DescribeRouteTablesRequest {
2553    #[serde(skip_serializing_if = "Option::is_none")]
2554    pub route_table_ids: Option<Vec<String>>,
2555    #[serde(skip_serializing_if = "Option::is_none")]
2556    pub filters: Option<Vec<Filter>>,
2557    #[serde(skip_serializing_if = "Option::is_none")]
2558    pub max_results: Option<i32>,
2559    #[serde(skip_serializing_if = "Option::is_none")]
2560    pub next_token: Option<String>,
2561}
2562
2563/// Response from describing route tables.
2564#[derive(Debug, Deserialize)]
2565#[serde(rename_all = "camelCase")]
2566pub struct DescribeRouteTablesResponse {
2567    #[serde(rename = "routeTableSet")]
2568    pub route_table_set: Option<RouteTableSet>,
2569    #[serde(rename = "nextToken")]
2570    pub next_token: Option<String>,
2571}
2572
2573#[derive(Debug, Deserialize)]
2574#[serde(rename_all = "camelCase")]
2575pub struct RouteTableSet {
2576    #[serde(rename = "item", default)]
2577    pub items: Vec<RouteTable>,
2578}
2579
2580/// Represents a route table.
2581#[derive(Debug, Clone, Deserialize)]
2582#[serde(rename_all = "camelCase")]
2583pub struct RouteTable {
2584    pub route_table_id: Option<String>,
2585    pub vpc_id: Option<String>,
2586    #[serde(rename = "routeSet")]
2587    pub route_set: Option<RouteSet>,
2588    #[serde(rename = "associationSet")]
2589    pub association_set: Option<RouteTableAssociationSet>,
2590    #[serde(rename = "tagSet")]
2591    pub tag_set: Option<TagSet>,
2592}
2593
2594#[derive(Debug, Clone, Deserialize)]
2595#[serde(rename_all = "camelCase")]
2596pub struct RouteSet {
2597    #[serde(rename = "item", default)]
2598    pub items: Vec<Route>,
2599}
2600
2601/// Represents a route in a route table.
2602#[derive(Debug, Clone, Deserialize)]
2603#[serde(rename_all = "camelCase")]
2604pub struct Route {
2605    pub destination_cidr_block: Option<String>,
2606    pub gateway_id: Option<String>,
2607    pub nat_gateway_id: Option<String>,
2608    pub instance_id: Option<String>,
2609    pub network_interface_id: Option<String>,
2610    pub vpc_peering_connection_id: Option<String>,
2611    pub transit_gateway_id: Option<String>,
2612    pub state: Option<String>,
2613    pub origin: Option<String>,
2614}
2615
2616#[derive(Debug, Clone, Deserialize)]
2617#[serde(rename_all = "camelCase")]
2618pub struct RouteTableAssociationSet {
2619    #[serde(rename = "item", default)]
2620    pub items: Vec<RouteTableAssociation>,
2621}
2622
2623/// Represents an association between a route table and a subnet.
2624#[derive(Debug, Clone, Deserialize)]
2625#[serde(rename_all = "camelCase")]
2626pub struct RouteTableAssociation {
2627    pub route_table_association_id: Option<String>,
2628    pub route_table_id: Option<String>,
2629    pub subnet_id: Option<String>,
2630    pub main: Option<bool>,
2631}
2632
2633/// Request to create a route table.
2634#[derive(Debug, Clone, Serialize, Builder)]
2635pub struct CreateRouteTableRequest {
2636    pub vpc_id: String,
2637    #[serde(skip_serializing_if = "Option::is_none")]
2638    pub tag_specifications: Option<Vec<TagSpecification>>,
2639}
2640
2641/// Response from creating a route table.
2642#[derive(Debug, Deserialize)]
2643#[serde(rename_all = "camelCase")]
2644pub struct CreateRouteTableResponse {
2645    #[serde(rename = "routeTable")]
2646    pub route_table: Option<RouteTable>,
2647}
2648
2649/// Request to create a route.
2650#[derive(Debug, Clone, Serialize, Builder)]
2651pub struct CreateRouteRequest {
2652    pub route_table_id: String,
2653    pub destination_cidr_block: String,
2654    #[serde(skip_serializing_if = "Option::is_none")]
2655    pub gateway_id: Option<String>,
2656    #[serde(skip_serializing_if = "Option::is_none")]
2657    pub nat_gateway_id: Option<String>,
2658    #[serde(skip_serializing_if = "Option::is_none")]
2659    pub instance_id: Option<String>,
2660    #[serde(skip_serializing_if = "Option::is_none")]
2661    pub network_interface_id: Option<String>,
2662    #[serde(skip_serializing_if = "Option::is_none")]
2663    pub vpc_peering_connection_id: Option<String>,
2664    #[serde(skip_serializing_if = "Option::is_none")]
2665    pub transit_gateway_id: Option<String>,
2666}
2667
2668/// Request to delete a route.
2669#[derive(Debug, Clone, Serialize, Builder)]
2670pub struct DeleteRouteRequest {
2671    pub route_table_id: String,
2672    pub destination_cidr_block: String,
2673}
2674
2675/// Request to associate a route table with a subnet.
2676#[derive(Debug, Clone, Serialize, Builder)]
2677pub struct AssociateRouteTableRequest {
2678    pub route_table_id: String,
2679    pub subnet_id: String,
2680}
2681
2682/// Response from associating a route table.
2683#[derive(Debug, Deserialize)]
2684#[serde(rename_all = "camelCase")]
2685pub struct AssociateRouteTableResponse {
2686    pub association_id: Option<String>,
2687}
2688
2689// ---------------------------------------------------------------------------
2690// Security Group Request/Response Types
2691// ---------------------------------------------------------------------------
2692
2693/// Request to describe security groups.
2694#[derive(Debug, Clone, Serialize, Builder, Default)]
2695pub struct DescribeSecurityGroupsRequest {
2696    #[serde(skip_serializing_if = "Option::is_none")]
2697    pub group_ids: Option<Vec<String>>,
2698    #[serde(skip_serializing_if = "Option::is_none")]
2699    pub group_names: Option<Vec<String>>,
2700    #[serde(skip_serializing_if = "Option::is_none")]
2701    pub filters: Option<Vec<Filter>>,
2702    #[serde(skip_serializing_if = "Option::is_none")]
2703    pub max_results: Option<i32>,
2704    #[serde(skip_serializing_if = "Option::is_none")]
2705    pub next_token: Option<String>,
2706}
2707
2708/// Response from describing security groups.
2709#[derive(Debug, Deserialize)]
2710#[serde(rename_all = "camelCase")]
2711pub struct DescribeSecurityGroupsResponse {
2712    #[serde(rename = "securityGroupInfo")]
2713    pub security_group_info: Option<SecurityGroupSet>,
2714    #[serde(rename = "nextToken")]
2715    pub next_token: Option<String>,
2716}
2717
2718#[derive(Debug, Deserialize)]
2719#[serde(rename_all = "camelCase")]
2720pub struct SecurityGroupSet {
2721    #[serde(rename = "item", default)]
2722    pub items: Vec<SecurityGroup>,
2723}
2724
2725/// Represents a security group.
2726#[derive(Debug, Clone, Deserialize)]
2727#[serde(rename_all = "camelCase")]
2728pub struct SecurityGroup {
2729    pub group_id: Option<String>,
2730    pub group_name: Option<String>,
2731    pub vpc_id: Option<String>,
2732    pub owner_id: Option<String>,
2733    pub group_description: Option<String>,
2734    #[serde(rename = "ipPermissions")]
2735    pub ip_permissions: Option<IpPermissionSet>,
2736    #[serde(rename = "ipPermissionsEgress")]
2737    pub ip_permissions_egress: Option<IpPermissionSet>,
2738    #[serde(rename = "tagSet")]
2739    pub tag_set: Option<TagSet>,
2740}
2741
2742/// Request to describe network interfaces.
2743#[derive(Debug, Clone, Serialize, Builder, Default)]
2744pub struct DescribeNetworkInterfacesRequest {
2745    #[serde(skip_serializing_if = "Option::is_none")]
2746    pub network_interface_ids: Option<Vec<String>>,
2747    #[serde(skip_serializing_if = "Option::is_none")]
2748    pub filters: Option<Vec<Filter>>,
2749    #[serde(skip_serializing_if = "Option::is_none")]
2750    pub max_results: Option<i32>,
2751    #[serde(skip_serializing_if = "Option::is_none")]
2752    pub next_token: Option<String>,
2753}
2754
2755/// Response from describing network interfaces.
2756#[derive(Debug, Deserialize)]
2757#[serde(rename_all = "camelCase")]
2758pub struct DescribeNetworkInterfacesResponse {
2759    #[serde(rename = "networkInterfaceSet")]
2760    pub network_interface_set: Option<NetworkInterfaceSet>,
2761    #[serde(rename = "nextToken")]
2762    pub next_token: Option<String>,
2763}
2764
2765#[derive(Debug, Deserialize)]
2766#[serde(rename_all = "camelCase")]
2767pub struct NetworkInterfaceSet {
2768    #[serde(rename = "item", default)]
2769    pub items: Vec<NetworkInterface>,
2770}
2771
2772/// Represents an EC2 network interface.
2773#[derive(Debug, Clone, Deserialize)]
2774#[serde(rename_all = "camelCase")]
2775pub struct NetworkInterface {
2776    pub network_interface_id: Option<String>,
2777    pub status: Option<String>,
2778    pub description: Option<String>,
2779    pub subnet_id: Option<String>,
2780    pub vpc_id: Option<String>,
2781    #[serde(rename = "groupSet")]
2782    pub group_set: Option<GroupIdentifierSet>,
2783}
2784
2785#[derive(Debug, Clone, Deserialize)]
2786#[serde(rename_all = "camelCase")]
2787pub struct GroupIdentifierSet {
2788    #[serde(rename = "item", default)]
2789    pub items: Vec<GroupIdentifier>,
2790}
2791
2792#[derive(Debug, Clone, Deserialize)]
2793#[serde(rename_all = "camelCase")]
2794pub struct GroupIdentifier {
2795    pub group_id: Option<String>,
2796    pub group_name: Option<String>,
2797}
2798
2799#[derive(Debug, Clone, Deserialize)]
2800#[serde(rename_all = "camelCase")]
2801pub struct IpPermissionSet {
2802    #[serde(rename = "item", default)]
2803    pub items: Vec<IpPermissionResponse>,
2804}
2805
2806/// IP permission in a response (from DescribeSecurityGroups).
2807#[derive(Debug, Clone, Deserialize)]
2808#[serde(rename_all = "camelCase")]
2809pub struct IpPermissionResponse {
2810    pub ip_protocol: Option<String>,
2811    pub from_port: Option<i32>,
2812    pub to_port: Option<i32>,
2813    #[serde(rename = "ipRanges")]
2814    pub ip_ranges: Option<IpRangeSet>,
2815    #[serde(rename = "ipv6Ranges")]
2816    pub ipv6_ranges: Option<Ipv6RangeSet>,
2817    #[serde(rename = "groups")]
2818    pub groups: Option<UserIdGroupPairSet>,
2819}
2820
2821#[derive(Debug, Clone, Deserialize)]
2822#[serde(rename_all = "camelCase")]
2823pub struct IpRangeSet {
2824    #[serde(rename = "item", default)]
2825    pub items: Vec<IpRangeResponse>,
2826}
2827
2828#[derive(Debug, Clone, Deserialize)]
2829#[serde(rename_all = "camelCase")]
2830pub struct IpRangeResponse {
2831    pub cidr_ip: Option<String>,
2832    pub description: Option<String>,
2833}
2834
2835#[derive(Debug, Clone, Deserialize)]
2836#[serde(rename_all = "camelCase")]
2837pub struct Ipv6RangeSet {
2838    #[serde(rename = "item", default)]
2839    pub items: Vec<Ipv6RangeResponse>,
2840}
2841
2842#[derive(Debug, Clone, Deserialize)]
2843#[serde(rename_all = "camelCase")]
2844pub struct Ipv6RangeResponse {
2845    pub cidr_ipv6: Option<String>,
2846    pub description: Option<String>,
2847}
2848
2849#[derive(Debug, Clone, Deserialize)]
2850#[serde(rename_all = "camelCase")]
2851pub struct UserIdGroupPairSet {
2852    #[serde(rename = "item", default)]
2853    pub items: Vec<UserIdGroupPairResponse>,
2854}
2855
2856#[derive(Debug, Clone, Deserialize)]
2857#[serde(rename_all = "camelCase")]
2858pub struct UserIdGroupPairResponse {
2859    pub group_id: Option<String>,
2860    pub user_id: Option<String>,
2861    pub description: Option<String>,
2862}
2863
2864/// Request to create a security group.
2865#[derive(Debug, Clone, Serialize, Builder)]
2866pub struct CreateSecurityGroupRequest {
2867    pub group_name: String,
2868    pub description: String,
2869    pub vpc_id: String,
2870    #[serde(skip_serializing_if = "Option::is_none")]
2871    pub tag_specifications: Option<Vec<TagSpecification>>,
2872}
2873
2874/// Response from creating a security group.
2875#[derive(Debug, Deserialize)]
2876#[serde(rename_all = "camelCase")]
2877pub struct CreateSecurityGroupResponse {
2878    pub group_id: Option<String>,
2879}
2880
2881/// IP permission for requests (authorize/revoke).
2882#[derive(Debug, Clone, Serialize, Builder)]
2883pub struct IpPermission {
2884    /// The IP protocol: tcp, udp, icmp, or -1 for all.
2885    pub ip_protocol: String,
2886    /// The start of port range (or ICMP type).
2887    #[serde(skip_serializing_if = "Option::is_none")]
2888    pub from_port: Option<i32>,
2889    /// The end of port range (or ICMP code).
2890    #[serde(skip_serializing_if = "Option::is_none")]
2891    pub to_port: Option<i32>,
2892    /// The IPv4 CIDR ranges.
2893    #[serde(skip_serializing_if = "Option::is_none")]
2894    pub ip_ranges: Option<Vec<IpRange>>,
2895    /// The IPv6 CIDR ranges.
2896    #[serde(skip_serializing_if = "Option::is_none")]
2897    pub ipv6_ranges: Option<Vec<Ipv6Range>>,
2898    /// The security group and AWS account ID pairs.
2899    #[serde(skip_serializing_if = "Option::is_none")]
2900    pub user_id_group_pairs: Option<Vec<UserIdGroupPair>>,
2901}
2902
2903/// IPv4 CIDR range for security group rules.
2904#[derive(Debug, Clone, Serialize, Builder)]
2905pub struct IpRange {
2906    pub cidr_ip: String,
2907    #[serde(skip_serializing_if = "Option::is_none")]
2908    pub description: Option<String>,
2909}
2910
2911/// IPv6 CIDR range for security group rules.
2912#[derive(Debug, Clone, Serialize, Builder)]
2913pub struct Ipv6Range {
2914    pub cidr_ipv6: String,
2915    #[serde(skip_serializing_if = "Option::is_none")]
2916    pub description: Option<String>,
2917}
2918
2919/// Security group and AWS account ID pair for security group rules.
2920#[derive(Debug, Clone, Serialize, Builder)]
2921pub struct UserIdGroupPair {
2922    #[serde(skip_serializing_if = "Option::is_none")]
2923    pub group_id: Option<String>,
2924    #[serde(skip_serializing_if = "Option::is_none")]
2925    pub user_id: Option<String>,
2926    #[serde(skip_serializing_if = "Option::is_none")]
2927    pub description: Option<String>,
2928}
2929
2930/// Request to authorize security group ingress.
2931#[derive(Debug, Clone, Serialize, Builder)]
2932pub struct AuthorizeSecurityGroupIngressRequest {
2933    pub group_id: String,
2934    pub ip_permissions: Vec<IpPermission>,
2935}
2936
2937/// Request to authorize security group egress.
2938#[derive(Debug, Clone, Serialize, Builder)]
2939pub struct AuthorizeSecurityGroupEgressRequest {
2940    pub group_id: String,
2941    pub ip_permissions: Vec<IpPermission>,
2942}
2943
2944/// Request to revoke security group ingress.
2945#[derive(Debug, Clone, Serialize, Builder)]
2946pub struct RevokeSecurityGroupIngressRequest {
2947    pub group_id: String,
2948    pub ip_permissions: Vec<IpPermission>,
2949}
2950
2951/// Request to revoke security group egress.
2952#[derive(Debug, Clone, Serialize, Builder)]
2953pub struct RevokeSecurityGroupEgressRequest {
2954    pub group_id: String,
2955    pub ip_permissions: Vec<IpPermission>,
2956}
2957
2958// ---------------------------------------------------------------------------
2959// Availability Zone Request/Response Types
2960// ---------------------------------------------------------------------------
2961
2962/// Request to describe availability zones.
2963#[derive(Debug, Clone, Serialize, Builder, Default)]
2964pub struct DescribeAvailabilityZonesRequest {
2965    #[serde(skip_serializing_if = "Option::is_none")]
2966    pub zone_names: Option<Vec<String>>,
2967    #[serde(skip_serializing_if = "Option::is_none")]
2968    pub zone_ids: Option<Vec<String>>,
2969    #[serde(skip_serializing_if = "Option::is_none")]
2970    pub filters: Option<Vec<Filter>>,
2971    #[serde(skip_serializing_if = "Option::is_none")]
2972    pub all_availability_zones: Option<bool>,
2973}
2974
2975/// Response from describing availability zones.
2976#[derive(Debug, Deserialize)]
2977#[serde(rename_all = "camelCase")]
2978pub struct DescribeAvailabilityZonesResponse {
2979    #[serde(rename = "availabilityZoneInfo")]
2980    pub availability_zone_info: Option<AvailabilityZoneSet>,
2981}
2982
2983#[derive(Debug, Deserialize)]
2984#[serde(rename_all = "camelCase")]
2985pub struct AvailabilityZoneSet {
2986    #[serde(rename = "item", default)]
2987    pub items: Vec<AvailabilityZone>,
2988}
2989
2990/// Represents an availability zone.
2991#[derive(Debug, Clone, Deserialize)]
2992#[serde(rename_all = "camelCase")]
2993pub struct AvailabilityZone {
2994    pub zone_name: Option<String>,
2995    pub zone_id: Option<String>,
2996    pub zone_state: Option<String>,
2997    pub region_name: Option<String>,
2998    pub zone_type: Option<String>,
2999    pub opt_in_status: Option<String>,
3000}
3001
3002// ---------------------------------------------------------------------------
3003// AMI Request/Response Types
3004// ---------------------------------------------------------------------------
3005
3006/// Request to describe images (AMIs).
3007#[derive(Debug, Clone, Serialize, Builder, Default)]
3008pub struct DescribeImagesRequest {
3009    /// The image IDs.
3010    #[serde(skip_serializing_if = "Option::is_none")]
3011    pub image_ids: Option<Vec<String>>,
3012    /// The owners (self, amazon, aws-marketplace, or account ID).
3013    #[serde(skip_serializing_if = "Option::is_none")]
3014    pub owners: Option<Vec<String>>,
3015    /// Users that have explicit launch permissions.
3016    #[serde(skip_serializing_if = "Option::is_none")]
3017    pub executable_users: Option<Vec<String>>,
3018    /// Filters for the images.
3019    #[serde(skip_serializing_if = "Option::is_none")]
3020    pub filters: Option<Vec<Filter>>,
3021    /// Whether to include deprecated AMIs.
3022    #[serde(skip_serializing_if = "Option::is_none")]
3023    pub include_deprecated: Option<bool>,
3024    /// Maximum results.
3025    #[serde(skip_serializing_if = "Option::is_none")]
3026    pub max_results: Option<i32>,
3027    /// Token for pagination.
3028    #[serde(skip_serializing_if = "Option::is_none")]
3029    pub next_token: Option<String>,
3030}
3031
3032/// Response from describing images.
3033#[derive(Debug, Deserialize)]
3034#[serde(rename_all = "camelCase")]
3035pub struct DescribeImagesResponse {
3036    #[serde(rename = "imagesSet")]
3037    pub images_set: Option<ImageSet>,
3038    #[serde(rename = "nextToken")]
3039    pub next_token: Option<String>,
3040}
3041
3042#[derive(Debug, Deserialize)]
3043#[serde(rename_all = "camelCase")]
3044pub struct ImageSet {
3045    #[serde(rename = "item", default)]
3046    pub items: Vec<Image>,
3047}
3048
3049/// Represents an AMI.
3050#[derive(Debug, Clone, Deserialize)]
3051#[serde(rename_all = "camelCase")]
3052pub struct Image {
3053    pub image_id: Option<String>,
3054    pub name: Option<String>,
3055    pub description: Option<String>,
3056    pub image_location: Option<String>,
3057    pub state: Option<String>,
3058    pub owner_id: Option<String>,
3059    pub creation_date: Option<String>,
3060    pub architecture: Option<String>,
3061    pub platform: Option<String>,
3062    pub platform_details: Option<String>,
3063    pub image_type: Option<String>,
3064    pub root_device_type: Option<String>,
3065    pub root_device_name: Option<String>,
3066    pub virtualization_type: Option<String>,
3067    pub hypervisor: Option<String>,
3068    pub is_public: Option<bool>,
3069    pub deprecation_time: Option<String>,
3070    #[serde(rename = "tagSet")]
3071    pub tag_set: Option<TagSet>,
3072    #[serde(rename = "blockDeviceMapping")]
3073    pub block_device_mapping: Option<BlockDeviceMappingSet>,
3074}
3075
3076#[derive(Debug, Clone, Deserialize)]
3077#[serde(rename_all = "camelCase")]
3078pub struct BlockDeviceMappingSet {
3079    #[serde(rename = "item", default)]
3080    pub items: Vec<BlockDeviceMapping>,
3081}
3082
3083#[derive(Debug, Clone, Deserialize)]
3084#[serde(rename_all = "camelCase")]
3085pub struct BlockDeviceMapping {
3086    pub device_name: Option<String>,
3087    pub ebs: Option<EbsBlockDevice>,
3088    pub virtual_name: Option<String>,
3089    pub no_device: Option<String>,
3090}
3091
3092#[derive(Debug, Clone, Deserialize)]
3093#[serde(rename_all = "camelCase")]
3094pub struct EbsBlockDevice {
3095    pub snapshot_id: Option<String>,
3096    pub volume_size: Option<i32>,
3097    pub volume_type: Option<String>,
3098    pub delete_on_termination: Option<bool>,
3099    pub encrypted: Option<bool>,
3100    pub iops: Option<i32>,
3101    pub throughput: Option<i32>,
3102}
3103
3104// ---------------------------------------------------------------------------
3105// Instance Request/Response Types
3106// ---------------------------------------------------------------------------
3107
3108/// Response from terminating instances.
3109#[derive(Debug, Deserialize)]
3110#[serde(rename_all = "camelCase")]
3111pub struct TerminateInstancesResponse {
3112    #[serde(rename = "instancesSet")]
3113    pub instances_set: Option<TerminatingInstanceSet>,
3114}
3115
3116#[derive(Debug, Deserialize)]
3117#[serde(rename_all = "camelCase")]
3118pub struct TerminatingInstanceSet {
3119    #[serde(rename = "item", default)]
3120    pub items: Vec<TerminatingInstance>,
3121}
3122
3123#[derive(Debug, Clone, Deserialize)]
3124#[serde(rename_all = "camelCase")]
3125pub struct TerminatingInstance {
3126    pub instance_id: Option<String>,
3127    pub current_state: Option<InstanceState>,
3128    pub previous_state: Option<InstanceState>,
3129}
3130
3131#[derive(Debug, Clone, Deserialize)]
3132#[serde(rename_all = "camelCase")]
3133pub struct InstanceState {
3134    pub code: Option<i32>,
3135    pub name: Option<String>,
3136}
3137
3138/// Request to describe instances.
3139#[derive(Debug, Clone, Serialize, Builder, Default)]
3140pub struct DescribeInstancesRequest {
3141    #[serde(skip_serializing_if = "Option::is_none")]
3142    pub instance_ids: Option<Vec<String>>,
3143    #[serde(skip_serializing_if = "Option::is_none")]
3144    pub filters: Option<Vec<Filter>>,
3145    #[serde(skip_serializing_if = "Option::is_none")]
3146    pub max_results: Option<i32>,
3147    #[serde(skip_serializing_if = "Option::is_none")]
3148    pub next_token: Option<String>,
3149}
3150
3151/// Response from describing instances.
3152#[derive(Debug, Deserialize)]
3153#[serde(rename_all = "camelCase")]
3154pub struct DescribeInstancesResponse {
3155    #[serde(rename = "reservationSet")]
3156    pub reservation_set: Option<ReservationSet>,
3157    #[serde(rename = "nextToken")]
3158    pub next_token: Option<String>,
3159}
3160
3161#[derive(Debug, Deserialize)]
3162#[serde(rename_all = "camelCase")]
3163pub struct ReservationSet {
3164    #[serde(rename = "item", default)]
3165    pub items: Vec<Reservation>,
3166}
3167
3168#[derive(Debug, Clone, Deserialize)]
3169#[serde(rename_all = "camelCase")]
3170pub struct Reservation {
3171    pub reservation_id: Option<String>,
3172    pub owner_id: Option<String>,
3173    #[serde(rename = "instancesSet")]
3174    pub instances_set: Option<InstanceSet>,
3175}
3176
3177#[derive(Debug, Clone, Deserialize)]
3178#[serde(rename_all = "camelCase")]
3179pub struct InstanceSet {
3180    #[serde(rename = "item", default)]
3181    pub items: Vec<Instance>,
3182}
3183
3184#[derive(Debug, Clone, Deserialize)]
3185#[serde(rename_all = "camelCase")]
3186pub struct Instance {
3187    pub instance_id: Option<String>,
3188    pub image_id: Option<String>,
3189    pub instance_type: Option<String>,
3190    pub key_name: Option<String>,
3191    pub launch_time: Option<String>,
3192    pub placement: Option<Placement>,
3193    pub private_dns_name: Option<String>,
3194    pub private_ip_address: Option<String>,
3195    pub public_dns_name: Option<String>,
3196    pub public_ip_address: Option<String>,
3197    pub state_reason: Option<StateReason>,
3198    pub instance_state: Option<InstanceState>,
3199    pub subnet_id: Option<String>,
3200    pub vpc_id: Option<String>,
3201    pub architecture: Option<String>,
3202    pub root_device_type: Option<String>,
3203    pub root_device_name: Option<String>,
3204    #[serde(rename = "tagSet")]
3205    pub tag_set: Option<TagSet>,
3206}
3207
3208#[derive(Debug, Clone, Deserialize)]
3209#[serde(rename_all = "camelCase")]
3210pub struct Placement {
3211    pub availability_zone: Option<String>,
3212    pub tenancy: Option<String>,
3213}
3214
3215#[derive(Debug, Clone, Deserialize)]
3216#[serde(rename_all = "camelCase")]
3217pub struct StateReason {
3218    pub code: Option<String>,
3219    pub message: Option<String>,
3220}
3221
3222// ---------------------------------------------------------------------------
3223// Volume Request/Response Types
3224// ---------------------------------------------------------------------------
3225
3226/// Request to create a volume.
3227#[derive(Debug, Clone, Serialize, Builder)]
3228pub struct CreateVolumeRequest {
3229    /// The availability zone.
3230    pub availability_zone: String,
3231    /// Stable token used by EC2 to make retries idempotent.
3232    #[serde(skip_serializing_if = "Option::is_none")]
3233    pub client_token: Option<String>,
3234    /// The size of the volume in GiBs.
3235    #[serde(skip_serializing_if = "Option::is_none")]
3236    pub size: Option<i32>,
3237    /// The snapshot ID to create the volume from.
3238    #[serde(skip_serializing_if = "Option::is_none")]
3239    pub snapshot_id: Option<String>,
3240    /// The volume type: gp2, gp3, io1, io2, st1, sc1, standard.
3241    #[serde(skip_serializing_if = "Option::is_none")]
3242    pub volume_type: Option<String>,
3243    /// IOPS for io1, io2, or gp3.
3244    #[serde(skip_serializing_if = "Option::is_none")]
3245    pub iops: Option<i32>,
3246    /// Throughput for gp3 in MiB/s.
3247    #[serde(skip_serializing_if = "Option::is_none")]
3248    pub throughput: Option<i32>,
3249    /// Whether to encrypt the volume.
3250    #[serde(skip_serializing_if = "Option::is_none")]
3251    pub encrypted: Option<bool>,
3252    /// The KMS key ID for encryption.
3253    #[serde(skip_serializing_if = "Option::is_none")]
3254    pub kms_key_id: Option<String>,
3255    /// Tags for the volume.
3256    #[serde(skip_serializing_if = "Option::is_none")]
3257    pub tag_specifications: Option<Vec<TagSpecification>>,
3258}
3259
3260/// Response from creating a volume.
3261#[derive(Debug, Deserialize)]
3262#[serde(rename_all = "camelCase")]
3263pub struct CreateVolumeResponse {
3264    pub volume_id: Option<String>,
3265    pub size: Option<i32>,
3266    pub availability_zone: Option<String>,
3267    pub state: Option<String>,
3268    pub volume_type: Option<String>,
3269    pub iops: Option<i32>,
3270    pub throughput: Option<i32>,
3271    pub encrypted: Option<bool>,
3272    pub create_time: Option<String>,
3273    #[serde(rename = "tagSet")]
3274    pub tag_set: Option<TagSet>,
3275}
3276
3277/// Request to grow an EBS volume.
3278#[derive(Debug, Clone, Serialize, Builder)]
3279pub struct ModifyVolumeRequest {
3280    /// ID of the volume to modify.
3281    pub volume_id: String,
3282    /// Target size in GiB. EC2 does not permit shrinking a volume.
3283    pub size: i32,
3284}
3285
3286/// Response returned after starting a volume modification.
3287#[derive(Debug, Deserialize)]
3288#[serde(rename_all = "camelCase")]
3289pub struct ModifyVolumeResponse {
3290    pub volume_modification: Option<VolumeModification>,
3291}
3292
3293/// Request to inspect the latest modifications for EBS volumes.
3294#[derive(Debug, Clone, Serialize, Builder, Default)]
3295pub struct DescribeVolumesModificationsRequest {
3296    #[serde(skip_serializing_if = "Option::is_none")]
3297    pub volume_ids: Option<Vec<String>>,
3298    #[serde(skip_serializing_if = "Option::is_none")]
3299    pub filters: Option<Vec<Filter>>,
3300    #[serde(skip_serializing_if = "Option::is_none")]
3301    pub max_results: Option<i32>,
3302    #[serde(skip_serializing_if = "Option::is_none")]
3303    pub next_token: Option<String>,
3304}
3305
3306/// Response containing the latest requested volume modifications.
3307#[derive(Debug, Deserialize)]
3308#[serde(rename_all = "camelCase")]
3309pub struct DescribeVolumesModificationsResponse {
3310    #[serde(rename = "volumeModificationSet")]
3311    pub volume_modification_set: Option<VolumeModificationSet>,
3312    pub next_token: Option<String>,
3313}
3314
3315/// Collection of EBS volume modifications.
3316#[derive(Debug, Deserialize)]
3317#[serde(rename_all = "camelCase")]
3318pub struct VolumeModificationSet {
3319    #[serde(rename = "item", default)]
3320    pub items: Vec<VolumeModification>,
3321}
3322
3323/// Latest modification state reported by EC2 for an EBS volume.
3324#[derive(Debug, Clone, Deserialize)]
3325#[serde(rename_all = "camelCase")]
3326pub struct VolumeModification {
3327    pub volume_id: Option<String>,
3328    pub modification_state: Option<String>,
3329    pub status_message: Option<String>,
3330    pub progress: Option<i64>,
3331    pub original_size: Option<i32>,
3332    pub target_size: Option<i32>,
3333    pub start_time: Option<String>,
3334    pub end_time: Option<String>,
3335}
3336
3337/// Request to describe volumes.
3338#[derive(Debug, Clone, Serialize, Builder, Default)]
3339pub struct DescribeVolumesRequest {
3340    #[serde(skip_serializing_if = "Option::is_none")]
3341    pub volume_ids: Option<Vec<String>>,
3342    #[serde(skip_serializing_if = "Option::is_none")]
3343    pub filters: Option<Vec<Filter>>,
3344    #[serde(skip_serializing_if = "Option::is_none")]
3345    pub max_results: Option<i32>,
3346    #[serde(skip_serializing_if = "Option::is_none")]
3347    pub next_token: Option<String>,
3348}
3349
3350/// Response from describing volumes.
3351#[derive(Debug, Deserialize)]
3352#[serde(rename_all = "camelCase")]
3353pub struct DescribeVolumesResponse {
3354    #[serde(rename = "volumeSet")]
3355    pub volume_set: Option<VolumeSet>,
3356    #[serde(rename = "nextToken")]
3357    pub next_token: Option<String>,
3358}
3359
3360#[derive(Debug, Deserialize)]
3361#[serde(rename_all = "camelCase")]
3362pub struct VolumeSet {
3363    #[serde(rename = "item", default)]
3364    pub items: Vec<Volume>,
3365}
3366
3367/// Represents an EBS volume.
3368#[derive(Debug, Clone, Deserialize)]
3369#[serde(rename_all = "camelCase")]
3370pub struct Volume {
3371    pub volume_id: Option<String>,
3372    pub size: Option<i32>,
3373    pub availability_zone: Option<String>,
3374    #[serde(rename = "status")]
3375    pub state: Option<String>,
3376    pub volume_type: Option<String>,
3377    pub iops: Option<i32>,
3378    pub throughput: Option<i32>,
3379    pub encrypted: Option<bool>,
3380    pub snapshot_id: Option<String>,
3381    pub create_time: Option<String>,
3382    #[serde(rename = "attachmentSet")]
3383    pub attachment_set: Option<VolumeAttachmentSet>,
3384    #[serde(rename = "tagSet")]
3385    pub tag_set: Option<TagSet>,
3386}
3387
3388#[derive(Debug, Clone, Deserialize)]
3389#[serde(rename_all = "camelCase")]
3390pub struct VolumeAttachmentSet {
3391    #[serde(rename = "item", default)]
3392    pub items: Vec<VolumeAttachment>,
3393}
3394
3395#[derive(Debug, Clone, Deserialize)]
3396#[serde(rename_all = "camelCase")]
3397pub struct VolumeAttachment {
3398    pub volume_id: Option<String>,
3399    pub instance_id: Option<String>,
3400    pub device: Option<String>,
3401    pub state: Option<String>,
3402    pub attach_time: Option<String>,
3403    pub delete_on_termination: Option<bool>,
3404}
3405
3406/// Request to attach a volume.
3407#[derive(Debug, Clone, Serialize, Builder)]
3408pub struct AttachVolumeRequest {
3409    /// The volume ID.
3410    pub volume_id: String,
3411    /// The instance ID.
3412    pub instance_id: String,
3413    /// The device name (e.g., /dev/sdf).
3414    pub device: String,
3415}
3416
3417/// Response from attaching a volume.
3418#[derive(Debug, Deserialize)]
3419#[serde(rename_all = "camelCase")]
3420pub struct AttachVolumeResponse {
3421    pub volume_id: Option<String>,
3422    pub instance_id: Option<String>,
3423    pub device: Option<String>,
3424    pub state: Option<String>,
3425    pub attach_time: Option<String>,
3426}
3427
3428/// Request to detach a volume.
3429#[derive(Debug, Clone, Serialize, Builder)]
3430pub struct DetachVolumeRequest {
3431    /// The volume ID.
3432    pub volume_id: String,
3433    /// The instance ID.
3434    #[serde(skip_serializing_if = "Option::is_none")]
3435    pub instance_id: Option<String>,
3436    /// The device name.
3437    #[serde(skip_serializing_if = "Option::is_none")]
3438    pub device: Option<String>,
3439    /// Whether to force detach.
3440    #[serde(skip_serializing_if = "Option::is_none")]
3441    pub force: Option<bool>,
3442}
3443
3444/// Response from detaching a volume.
3445#[derive(Debug, Deserialize)]
3446#[serde(rename_all = "camelCase")]
3447pub struct DetachVolumeResponse {
3448    pub volume_id: Option<String>,
3449    pub instance_id: Option<String>,
3450    pub device: Option<String>,
3451    pub state: Option<String>,
3452    pub attach_time: Option<String>,
3453}
3454
3455// ---------------------------------------------------------------------------
3456// Launch Template Request/Response Types
3457// ---------------------------------------------------------------------------
3458
3459/// Request to create a launch template.
3460#[derive(Debug, Clone, Serialize, Builder)]
3461pub struct CreateLaunchTemplateRequest {
3462    /// The name for the launch template.
3463    pub launch_template_name: String,
3464    /// A description for the launch template.
3465    #[serde(skip_serializing_if = "Option::is_none")]
3466    pub version_description: Option<String>,
3467    /// The launch template data.
3468    pub launch_template_data: RequestLaunchTemplateData,
3469    /// Tags for the launch template.
3470    #[serde(skip_serializing_if = "Option::is_none")]
3471    pub tag_specifications: Option<Vec<TagSpecification>>,
3472}
3473
3474/// Launch template data for the request.
3475#[derive(Debug, Clone, Serialize, Builder, Default)]
3476pub struct RequestLaunchTemplateData {
3477    /// The ID of the AMI.
3478    #[serde(skip_serializing_if = "Option::is_none")]
3479    pub image_id: Option<String>,
3480    /// The instance type.
3481    #[serde(skip_serializing_if = "Option::is_none")]
3482    pub instance_type: Option<String>,
3483    /// The key name.
3484    #[serde(skip_serializing_if = "Option::is_none")]
3485    pub key_name: Option<String>,
3486    /// The user data (base64-encoded).
3487    #[serde(skip_serializing_if = "Option::is_none")]
3488    pub user_data: Option<String>,
3489    /// The security group IDs.
3490    #[serde(skip_serializing_if = "Option::is_none")]
3491    pub security_group_ids: Option<Vec<String>>,
3492    /// The IAM instance profile.
3493    #[serde(skip_serializing_if = "Option::is_none")]
3494    pub iam_instance_profile: Option<LaunchTemplateIamInstanceProfileSpecification>,
3495    /// Block device mappings.
3496    #[serde(skip_serializing_if = "Option::is_none")]
3497    pub block_device_mappings: Option<Vec<LaunchTemplateBlockDeviceMapping>>,
3498    /// Network interfaces.
3499    #[serde(skip_serializing_if = "Option::is_none")]
3500    pub network_interfaces: Option<Vec<LaunchTemplateNetworkInterface>>,
3501    /// Metadata options.
3502    #[serde(skip_serializing_if = "Option::is_none")]
3503    pub metadata_options: Option<LaunchTemplateInstanceMetadataOptions>,
3504    /// CPU options (currently only `NestedVirtualization`).
3505    #[serde(skip_serializing_if = "Option::is_none")]
3506    pub cpu_options: Option<LaunchTemplateCpuOptions>,
3507    /// Tags to apply to resources created from the launch template.
3508    #[serde(skip_serializing_if = "Option::is_none")]
3509    pub tag_specifications: Option<Vec<TagSpecification>>,
3510}
3511
3512/// IAM instance profile specification for launch template.
3513#[derive(Debug, Clone, Serialize, Builder)]
3514pub struct LaunchTemplateIamInstanceProfileSpecification {
3515    #[serde(skip_serializing_if = "Option::is_none")]
3516    pub arn: Option<String>,
3517    #[serde(skip_serializing_if = "Option::is_none")]
3518    pub name: Option<String>,
3519}
3520
3521/// Block device mapping for launch template.
3522#[derive(Debug, Clone, Serialize, Builder)]
3523pub struct LaunchTemplateBlockDeviceMapping {
3524    #[serde(skip_serializing_if = "Option::is_none")]
3525    pub device_name: Option<String>,
3526    #[serde(skip_serializing_if = "Option::is_none")]
3527    pub ebs: Option<LaunchTemplateEbsBlockDevice>,
3528}
3529
3530/// EBS block device for launch template.
3531#[derive(Debug, Clone, Serialize, Builder)]
3532pub struct LaunchTemplateEbsBlockDevice {
3533    #[serde(skip_serializing_if = "Option::is_none")]
3534    pub volume_size: Option<i32>,
3535    #[serde(skip_serializing_if = "Option::is_none")]
3536    pub volume_type: Option<String>,
3537    #[serde(skip_serializing_if = "Option::is_none")]
3538    pub delete_on_termination: Option<bool>,
3539    #[serde(skip_serializing_if = "Option::is_none")]
3540    pub encrypted: Option<bool>,
3541    #[serde(skip_serializing_if = "Option::is_none")]
3542    pub iops: Option<i32>,
3543    #[serde(skip_serializing_if = "Option::is_none")]
3544    pub throughput: Option<i32>,
3545}
3546
3547/// Network interface for launch template.
3548#[derive(Debug, Clone, Serialize, Builder)]
3549pub struct LaunchTemplateNetworkInterface {
3550    #[serde(skip_serializing_if = "Option::is_none")]
3551    pub device_index: Option<i32>,
3552    #[serde(skip_serializing_if = "Option::is_none")]
3553    pub associate_public_ip_address: Option<bool>,
3554    #[serde(skip_serializing_if = "Option::is_none")]
3555    pub subnet_id: Option<String>,
3556    #[serde(skip_serializing_if = "Option::is_none")]
3557    pub groups: Option<Vec<String>>,
3558}
3559
3560/// Instance metadata options for launch template.
3561#[derive(Debug, Clone, Serialize, Builder)]
3562pub struct LaunchTemplateInstanceMetadataOptions {
3563    #[serde(skip_serializing_if = "Option::is_none")]
3564    pub http_tokens: Option<String>,
3565    #[serde(skip_serializing_if = "Option::is_none")]
3566    pub http_endpoint: Option<String>,
3567    #[serde(skip_serializing_if = "Option::is_none")]
3568    pub http_put_response_hop_limit: Option<i32>,
3569    #[serde(skip_serializing_if = "Option::is_none")]
3570    pub instance_metadata_tags: Option<String>,
3571}
3572
3573/// CPU options for launch template.
3574///
3575/// AWS only accepts `nested_virtualization = "enabled"` on 8th-generation
3576/// Intel instance types (c8i, m8i, r8i, and their flex variants). Other
3577/// instance types will reject the launch with a clear error — we let AWS
3578/// be the authority on supported types rather than maintaining our own
3579/// allowlist.
3580///
3581/// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateCpuOptionsRequest.html
3582#[derive(Debug, Clone, Serialize, Builder, Default)]
3583pub struct LaunchTemplateCpuOptions {
3584    /// "enabled" or "disabled". Omit to leave at AWS default ("disabled").
3585    #[serde(skip_serializing_if = "Option::is_none")]
3586    pub nested_virtualization: Option<String>,
3587}
3588
3589/// Response from creating a launch template.
3590#[derive(Debug, Deserialize)]
3591#[serde(rename_all = "camelCase")]
3592pub struct CreateLaunchTemplateResponse {
3593    pub launch_template: Option<LaunchTemplate>,
3594}
3595
3596/// Request to create a new version of an existing launch template.
3597/// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateLaunchTemplateVersion.html
3598#[derive(Debug, Clone, Builder, Default)]
3599pub struct CreateLaunchTemplateVersionRequest {
3600    pub launch_template_id: Option<String>,
3601    pub launch_template_name: Option<String>,
3602    pub source_version: Option<String>,
3603    pub version_description: Option<String>,
3604    pub launch_template_data: RequestLaunchTemplateData,
3605}
3606
3607/// Response from creating a launch template version.
3608#[derive(Debug, Deserialize)]
3609pub struct CreateLaunchTemplateVersionResponse {
3610    #[serde(rename = "launchTemplateVersion")]
3611    pub launch_template_version: Option<LaunchTemplateVersion>,
3612}
3613
3614/// A version of a launch template.
3615#[derive(Debug, Clone, Deserialize)]
3616pub struct LaunchTemplateVersion {
3617    #[serde(rename = "launchTemplateId")]
3618    pub launch_template_id: Option<String>,
3619    #[serde(rename = "versionNumber")]
3620    pub version_number: Option<i64>,
3621}
3622
3623/// Request to delete a launch template.
3624#[derive(Debug, Clone, Serialize, Builder, Default)]
3625pub struct DeleteLaunchTemplateRequest {
3626    #[serde(skip_serializing_if = "Option::is_none")]
3627    pub launch_template_id: Option<String>,
3628    #[serde(skip_serializing_if = "Option::is_none")]
3629    pub launch_template_name: Option<String>,
3630}
3631
3632/// Response from deleting a launch template.
3633#[derive(Debug, Deserialize)]
3634#[serde(rename_all = "camelCase")]
3635pub struct DeleteLaunchTemplateResponse {
3636    pub launch_template: Option<LaunchTemplate>,
3637}
3638
3639/// Request to describe launch templates.
3640#[derive(Debug, Clone, Serialize, Builder, Default)]
3641pub struct DescribeLaunchTemplatesRequest {
3642    #[serde(skip_serializing_if = "Option::is_none")]
3643    pub launch_template_ids: Option<Vec<String>>,
3644    #[serde(skip_serializing_if = "Option::is_none")]
3645    pub launch_template_names: Option<Vec<String>>,
3646    #[serde(skip_serializing_if = "Option::is_none")]
3647    pub filters: Option<Vec<Filter>>,
3648    #[serde(skip_serializing_if = "Option::is_none")]
3649    pub max_results: Option<i32>,
3650    #[serde(skip_serializing_if = "Option::is_none")]
3651    pub next_token: Option<String>,
3652}
3653
3654/// Response from describing launch templates.
3655#[derive(Debug, Deserialize)]
3656#[serde(rename_all = "camelCase")]
3657pub struct DescribeLaunchTemplatesResponse {
3658    #[serde(rename = "launchTemplates")]
3659    pub launch_templates: Option<LaunchTemplateSet>,
3660    #[serde(rename = "nextToken")]
3661    pub next_token: Option<String>,
3662}
3663
3664#[derive(Debug, Deserialize)]
3665#[serde(rename_all = "camelCase")]
3666pub struct LaunchTemplateSet {
3667    #[serde(rename = "item", default)]
3668    pub items: Vec<LaunchTemplate>,
3669}
3670
3671/// Represents a launch template.
3672#[derive(Debug, Clone, Deserialize)]
3673#[serde(rename_all = "camelCase")]
3674pub struct LaunchTemplate {
3675    pub launch_template_id: Option<String>,
3676    pub launch_template_name: Option<String>,
3677    pub create_time: Option<String>,
3678    pub created_by: Option<String>,
3679    pub default_version_number: Option<i64>,
3680    pub latest_version_number: Option<i64>,
3681    #[serde(rename = "tagSet")]
3682    pub tag_set: Option<TagSet>,
3683}
3684
3685/// Response from the GetConsoleOutput API.
3686/// See: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetConsoleOutput.html
3687#[derive(Debug, Deserialize, Default)]
3688#[serde(rename_all = "camelCase")]
3689pub struct GetConsoleOutputResponse {
3690    /// The ID of the instance.
3691    #[serde(rename = "instanceId")]
3692    pub instance_id: Option<String>,
3693    /// The console output, base64-encoded.
3694    pub output: Option<String>,
3695    /// The time at which the output was last updated.
3696    pub timestamp: Option<String>,
3697}
3698
3699#[cfg(test)]
3700mod volume_operation_tests {
3701    use super::*;
3702
3703    #[test]
3704    fn create_volume_maps_idempotency_token_to_ec2_query_parameter() {
3705        let request = CreateVolumeRequest::builder()
3706            .availability_zone("us-west-2a".to_string())
3707            .client_token("deployment-disk-ordinal-3".to_string())
3708            .size(64)
3709            .volume_type("gp3".to_string())
3710            .encrypted(true)
3711            .build();
3712
3713        let form = Ec2Client::create_volume_form_data(&request);
3714
3715        assert_eq!(form.get("Action").map(String::as_str), Some("CreateVolume"));
3716        assert_eq!(
3717            form.get("ClientToken").map(String::as_str),
3718            Some("deployment-disk-ordinal-3")
3719        );
3720        assert_eq!(
3721            form.get("AvailabilityZone").map(String::as_str),
3722            Some("us-west-2a")
3723        );
3724        assert_eq!(form.get("Size").map(String::as_str), Some("64"));
3725        assert_eq!(form.get("VolumeType").map(String::as_str), Some("gp3"));
3726        assert_eq!(form.get("Encrypted").map(String::as_str), Some("true"));
3727    }
3728
3729    #[test]
3730    fn volume_growth_operations_map_to_ec2_query_parameters() {
3731        let modify = ModifyVolumeRequest::builder()
3732            .volume_id("vol-0123456789abcdef0".to_string())
3733            .size(128)
3734            .build();
3735        let modify_form = Ec2Client::modify_volume_form_data(&modify);
3736        assert_eq!(
3737            modify_form.get("Action").map(String::as_str),
3738            Some("ModifyVolume")
3739        );
3740        assert_eq!(
3741            modify_form.get("VolumeId").map(String::as_str),
3742            Some("vol-0123456789abcdef0")
3743        );
3744        assert_eq!(modify_form.get("Size").map(String::as_str), Some("128"));
3745
3746        let describe = DescribeVolumesModificationsRequest::builder()
3747            .volume_ids(vec![
3748                "vol-0123456789abcdef0".to_string(),
3749                "vol-0123456789abcdef1".to_string(),
3750            ])
3751            .max_results(2)
3752            .next_token("next-page".to_string())
3753            .build();
3754        let describe_form = Ec2Client::describe_volumes_modifications_form_data(&describe);
3755        assert_eq!(
3756            describe_form.get("Action").map(String::as_str),
3757            Some("DescribeVolumesModifications")
3758        );
3759        assert_eq!(
3760            describe_form.get("VolumeId.1").map(String::as_str),
3761            Some("vol-0123456789abcdef0")
3762        );
3763        assert_eq!(
3764            describe_form.get("VolumeId.2").map(String::as_str),
3765            Some("vol-0123456789abcdef1")
3766        );
3767        assert_eq!(
3768            describe_form.get("MaxResults").map(String::as_str),
3769            Some("2")
3770        );
3771        assert_eq!(
3772            describe_form.get("NextToken").map(String::as_str),
3773            Some("next-page")
3774        );
3775    }
3776
3777    #[test]
3778    fn volume_modification_responses_deserialize_from_ec2_xml() {
3779        let modify: ModifyVolumeResponse = quick_xml::de::from_str(
3780            r#"<ModifyVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
3781                <volumeModification>
3782                    <volumeId>vol-0123456789abcdef0</volumeId>
3783                    <modificationState>modifying</modificationState>
3784                    <progress>0</progress>
3785                    <originalSize>64</originalSize>
3786                    <targetSize>128</targetSize>
3787                </volumeModification>
3788            </ModifyVolumeResponse>"#,
3789        )
3790        .expect("ModifyVolume response should deserialize");
3791        let modification = modify
3792            .volume_modification
3793            .expect("ModifyVolume should include its modification");
3794        assert_eq!(
3795            modification.volume_id.as_deref(),
3796            Some("vol-0123456789abcdef0")
3797        );
3798        assert_eq!(
3799            modification.modification_state.as_deref(),
3800            Some("modifying")
3801        );
3802        assert_eq!(modification.progress, Some(0));
3803        assert_eq!(modification.original_size, Some(64));
3804        assert_eq!(modification.target_size, Some(128));
3805
3806        let described: DescribeVolumesModificationsResponse = quick_xml::de::from_str(
3807            r#"<DescribeVolumesModificationsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
3808                <volumeModificationSet>
3809                    <item>
3810                        <volumeId>vol-0123456789abcdef0</volumeId>
3811                        <modificationState>completed</modificationState>
3812                        <progress>100</progress>
3813                        <originalSize>64</originalSize>
3814                        <targetSize>128</targetSize>
3815                        <endTime>2026-07-20T10:00:00.000Z</endTime>
3816                    </item>
3817                </volumeModificationSet>
3818                <nextToken>next-page</nextToken>
3819            </DescribeVolumesModificationsResponse>"#,
3820        )
3821        .expect("DescribeVolumesModifications response should deserialize");
3822        let modifications = described
3823            .volume_modification_set
3824            .expect("response should include modifications");
3825        assert_eq!(modifications.items.len(), 1);
3826        assert_eq!(
3827            modifications.items[0].modification_state.as_deref(),
3828            Some("completed")
3829        );
3830        assert_eq!(modifications.items[0].progress, Some(100));
3831        assert_eq!(described.next_token.as_deref(), Some("next-page"));
3832    }
3833}
3834
3835impl GetConsoleOutputResponse {
3836    /// Decodes the base64-encoded output to a UTF-8 string.
3837    pub fn decode_output(&self) -> Option<String> {
3838        self.output.as_ref().and_then(|b64| {
3839            STANDARD
3840                .decode(b64.trim())
3841                .ok()
3842                .and_then(|bytes| String::from_utf8(bytes).ok())
3843        })
3844    }
3845}
3846
3847#[cfg(test)]
3848mod tests {
3849    use super::*;
3850
3851    #[test]
3852    fn describe_volumes_deserializes_aws_status_as_volume_state() {
3853        let response: DescribeVolumesResponse = quick_xml::de::from_str(
3854            r#"<DescribeVolumesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
3855                <volumeSet>
3856                    <item>
3857                        <volumeId>vol-0123456789abcdef0</volumeId>
3858                        <size>10</size>
3859                        <availabilityZone>us-west-2a</availabilityZone>
3860                        <status>available</status>
3861                        <volumeType>gp3</volumeType>
3862                    </item>
3863                </volumeSet>
3864            </DescribeVolumesResponse>"#,
3865        )
3866        .expect("AWS DescribeVolumes response should deserialize");
3867
3868        let volumes = response
3869            .volume_set
3870            .expect("volumeSet should be present")
3871            .items;
3872        assert_eq!(volumes.len(), 1);
3873        assert_eq!(
3874            volumes[0].volume_id.as_deref(),
3875            Some("vol-0123456789abcdef0")
3876        );
3877        assert_eq!(volumes[0].state.as_deref(), Some("available"));
3878    }
3879
3880    #[test]
3881    fn launch_template_already_exists_is_a_typed_resource_conflict() {
3882        let response = r#"<?xml version="1.0" encoding="UTF-8"?>
3883            <Response><Errors><Error>
3884                <Code>InvalidLaunchTemplateName.AlreadyExistsException</Code>
3885                <Message>Launch template name already in use.</Message>
3886            </Error></Errors><RequestID>request-id</RequestID></Response>"#;
3887
3888        let error = Ec2Client::map_ec2_error(
3889            StatusCode::BAD_REQUEST,
3890            response,
3891            "CreateLaunchTemplate",
3892            "deployment-compute-general-lt",
3893            Some("LaunchTemplateName=deployment-compute-general-lt"),
3894        );
3895
3896        assert!(matches!(
3897            error,
3898            Some(ErrorData::RemoteResourceConflict {
3899                resource_type,
3900                resource_name,
3901                ..
3902            }) if resource_type == "EC2 Resource"
3903                && resource_name == "deployment-compute-general-lt"
3904        ));
3905    }
3906
3907    #[test]
3908    fn missing_volume_is_a_typed_remote_resource_not_found() {
3909        let response = r#"<?xml version="1.0" encoding="UTF-8"?>
3910            <Response><Errors><Error>
3911                <Code>InvalidVolume.NotFound</Code>
3912                <Message>The volume does not exist.</Message>
3913            </Error></Errors><RequestID>request-id</RequestID></Response>"#;
3914
3915        let error = Ec2Client::map_ec2_error(
3916            StatusCode::BAD_REQUEST,
3917            response,
3918            "DescribeVolumesModifications",
3919            "vol-0123456789abcdef0",
3920            Some("VolumeId.1=vol-0123456789abcdef0"),
3921        );
3922
3923        assert!(matches!(
3924            error,
3925            Some(ErrorData::RemoteResourceNotFound {
3926                resource_type,
3927                resource_name,
3928            }) if resource_type == "Volume" && resource_name == "vol-0123456789abcdef0"
3929        ));
3930    }
3931
3932    /// Setting `nested_virtualization=Some("enabled")` emits the AWS
3933    /// form-encoded key `LaunchTemplateData.CpuOptions.NestedVirtualization`
3934    /// with value `enabled`. Locks in the exact wire-format string AWS
3935    /// rejects/accepts on the LT create endpoint.
3936    #[test]
3937    fn add_cpu_options_emits_nested_virtualization_key() {
3938        let mut form_data = HashMap::new();
3939        let cpu_options = LaunchTemplateCpuOptions::builder()
3940            .nested_virtualization("enabled".to_string())
3941            .build();
3942        Ec2Client::add_cpu_options(&mut form_data, Some(&cpu_options));
3943        assert_eq!(
3944            form_data.get("LaunchTemplateData.CpuOptions.NestedVirtualization"),
3945            Some(&"enabled".to_string())
3946        );
3947        assert_eq!(form_data.len(), 1);
3948    }
3949
3950    /// `None` cpu_options → nothing is appended. Guards against accidentally
3951    /// sending an empty `CpuOptions` block on non-privileged daemon deploys.
3952    #[test]
3953    fn add_cpu_options_with_none_emits_nothing() {
3954        let mut form_data = HashMap::new();
3955        Ec2Client::add_cpu_options(&mut form_data, None);
3956        assert!(form_data.is_empty());
3957    }
3958
3959    /// `Some(CpuOptions { nested_virtualization: None })` → nothing appended.
3960    /// Future fields on CpuOptions could be set independently; the encoder
3961    /// should not emit a key for an unset NestedVirtualization specifically.
3962    #[test]
3963    fn add_cpu_options_with_unset_nested_field_emits_nothing() {
3964        let mut form_data = HashMap::new();
3965        let cpu_options = LaunchTemplateCpuOptions::builder().build();
3966        Ec2Client::add_cpu_options(&mut form_data, Some(&cpu_options));
3967        assert!(form_data.is_empty());
3968    }
3969}