aws_lib/
lib.rs

1//! Provides an opinionated interface to the AWS API
2
3extern crate self as aws_lib;
4
5use std::{
6    fmt::{self, Debug},
7    net,
8    time::Duration,
9};
10
11use aws_config::retry::RetryConfig;
12use aws_sdk_ec2::client::Waiters;
13use chrono::{DateTime, Utc};
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16
17mod error;
18pub use error::Error;
19
20pub mod tags;
21use tags::{ParseTagValueError, RawTag, RawTagValue, Tag, TagKey, TagList};
22
23pub mod export;
24
25macro_rules! wrap_aws_enum {
26    ($name:ident) => {
27        #[derive(Debug, Clone)]
28        pub struct $name(aws_sdk_ec2::types::$name);
29
30        impl $name {
31            pub const fn new(from: aws_sdk_ec2::types::$name) -> Self {
32                Self(from)
33            }
34
35            pub const fn inner(&self) -> &aws_sdk_ec2::types::$name {
36                &self.0
37            }
38
39            pub fn into_inner(self) -> aws_sdk_ec2::types::$name {
40                self.0
41            }
42        }
43
44        #[cfg(feature = "serde")]
45        impl Serialize for $name {
46            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47            where
48                S: serde::Serializer,
49            {
50                serializer.serialize_str(&self.inner().as_str())
51            }
52        }
53
54        #[cfg(feature = "serde")]
55        impl<'de> Deserialize<'de> for $name {
56            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57            where
58                D: serde::Deserializer<'de>,
59            {
60                Ok(Self(String::deserialize(deserializer)?.as_str().into()))
61            }
62        }
63
64        impl fmt::Display for $name {
65            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66                write!(f, "{}", self.inner().to_string())
67            }
68        }
69    };
70}
71
72wrap_aws_enum!(InstanceStateName);
73wrap_aws_enum!(InstanceType);
74
75#[derive(Debug)]
76pub struct Instance {
77    tags: TagList,
78    instance_type: InstanceType,
79    state: InstanceStateName,
80    instance_id: InstanceId,
81    image_id: AmiId,
82    subnet_id: SubnetId,
83    public_ip_address: Option<Ip>,
84}
85
86impl Instance {
87    pub fn try_from_aws(instance: aws_sdk_ec2::types::Instance) -> Result<Self, Error> {
88        macro_rules! extract {
89            ($instance:ident, $field:ident) => {
90                $instance
91                    .$field
92                    .clone() // not ideal
93                    .ok_or_else(|| Error::UnexpectedNoneValue {
94                        entity: stringify!($field).to_owned(),
95                    })
96            };
97        }
98
99        Ok(Self {
100            tags: extract!(instance, tags)?.try_into()?,
101            instance_type: InstanceType(extract!(instance, instance_type)?),
102            state: InstanceStateName(extract!(instance, state)?.name.ok_or_else(|| {
103                Error::UnexpectedNoneValue {
104                    entity: "state.name".to_owned(),
105                }
106            })?),
107            instance_id: InstanceId(extract!(instance, instance_id)?),
108            image_id: AmiId(extract!(instance, image_id)?),
109            subnet_id: SubnetId(extract!(instance, subnet_id)?),
110            public_ip_address: instance
111                .public_ip_address
112                .map(|s| -> Result<_, Error> { Ok(Ip(s.parse()?)) })
113                .transpose()?,
114        })
115    }
116
117    pub fn get_tag(&self, key: TagKey) -> Option<&RawTag> {
118        self.tags.get(key)
119    }
120
121    pub const fn tags(&self) -> &TagList {
122        &self.tags
123    }
124
125    pub const fn instance_type(&self) -> &InstanceType {
126        &self.instance_type
127    }
128
129    pub const fn state(&self) -> &InstanceStateName {
130        &self.state
131    }
132
133    pub const fn instance_id(&self) -> &InstanceId {
134        &self.instance_id
135    }
136
137    pub const fn image_id(&self) -> &AmiId {
138        &self.image_id
139    }
140
141    pub const fn subnet_id(&self) -> &SubnetId {
142        &self.subnet_id
143    }
144
145    pub const fn public_ip_address(&self) -> Option<&Ip> {
146        self.public_ip_address.as_ref()
147    }
148
149    pub async fn stop(&self, client: &RegionClient) -> Result<(), Error> {
150        let _state_change_info = client
151            .main
152            .ec2
153            .stop_instances()
154            .instance_ids(self.instance_id().as_str())
155            .send()
156            .await?;
157
158        Ok(())
159    }
160
161    pub async fn wait_for_stop(
162        &self,
163        client: &RegionClient,
164        max_wait: Duration,
165    ) -> Result<(), Error> {
166        match client
167            .main
168            .ec2
169            .wait_until_instance_stopped()
170            .instance_ids(self.instance_id().as_str())
171            .wait(max_wait)
172            .await
173        {
174            Ok(_final_response) => Ok(()),
175            Err(e) => match e {
176                aws_sdk_ec2::waiters::instance_stopped::WaitUntilInstanceStoppedError::ExceededMaxWait(_) => Err(Error::InstanceStopExceededMaxWait { max_wait, instance: self.instance_id().clone()}),
177                _ => Err(e.into())
178            },
179        }?;
180
181        Ok(())
182    }
183
184    pub async fn add_tag<T>(&self, client: &RegionClient, tag: Tag<T>) -> Result<(), Error>
185    where
186        T: Debug + Clone + PartialEq + Eq + Into<String> + Send,
187        T: tags::TagValue<T>,
188    {
189        let _output = client
190            .main
191            .ec2
192            .create_tags()
193            .resources(self.instance_id().as_str())
194            .tags(tag.into())
195            .send()
196            .await?;
197
198        Ok(())
199    }
200}
201
202#[derive(Debug, Copy, Clone)]
203#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
204pub enum Region {
205    #[cfg_attr(feature = "serde", serde(rename = "eu-central-1"))]
206    EuCentral1,
207    #[cfg_attr(feature = "serde", serde(rename = "us-east-1"))]
208    UsEast1,
209}
210
211impl fmt::Display for Region {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(f, "{}", self.as_str())
214    }
215}
216
217#[derive(Debug)]
218pub struct ShieldPop(String);
219
220impl ShieldPop {
221    pub fn into_string(self) -> String {
222        self.0
223    }
224}
225
226impl Region {
227    pub const fn as_str(self) -> &'static str {
228        match self {
229            Self::EuCentral1 => "eu-central-1",
230            Self::UsEast1 => "us-east-1",
231        }
232    }
233
234    pub const fn all() -> [Self; 2] {
235        [Self::EuCentral1, Self::UsEast1]
236    }
237
238    const fn name(self) -> &'static str {
239        match self {
240            Self::EuCentral1 => "eu-central-1",
241            Self::UsEast1 => "us-east-1",
242        }
243    }
244
245    pub fn cdn_shield_pop(self) -> ShieldPop {
246        ShieldPop(
247            match self {
248                Self::EuCentral1 => "eu-central-1",
249                Self::UsEast1 => "us-east-1",
250            }
251            .to_owned(),
252        )
253    }
254}
255
256#[derive(Debug, Clone)]
257pub struct RegionClientMain {
258    pub ec2: aws_sdk_ec2::Client,
259    pub efs: aws_sdk_efs::Client,
260    pub route53: aws_sdk_route53::Client,
261}
262
263#[derive(Debug, Clone)]
264pub struct RegionClientCdn {
265    pub cloudfront: aws_sdk_cloudfront::Client,
266    pub cloudformation: aws_sdk_cloudformation::Client,
267}
268
269#[derive(Debug, Clone)]
270pub struct RegionClient {
271    pub region: Region,
272    pub main: RegionClientMain,
273    pub cdn: RegionClientCdn,
274}
275
276#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
277#[derive(Debug, Clone)]
278pub struct InstanceProfileName(String);
279
280impl InstanceProfileName {
281    pub fn as_str(&self) -> &str {
282        &self.0
283    }
284}
285
286#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
287#[derive(Debug, Clone)]
288pub struct InstanceKeypairName(String);
289
290impl InstanceKeypairName {
291    pub fn as_str(&self) -> &str {
292        &self.0
293    }
294}
295
296#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
297#[derive(Debug, Clone)]
298pub struct SecurityGroupId(String);
299
300impl SecurityGroupId {
301    pub fn as_str(&self) -> &str {
302        &self.0
303    }
304}
305
306#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
307#[derive(Debug, Clone)]
308pub struct SecurityGroup {
309    id: SecurityGroupId,
310}
311
312#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
313#[derive(Debug, Clone)]
314pub struct SubnetId(String);
315
316impl SubnetId {
317    pub fn as_str(&self) -> &str {
318        &self.0
319    }
320
321    pub const fn from_string(value: String) -> Self {
322        Self(value)
323    }
324}
325
326impl PartialEq for SubnetId {
327    fn eq(&self, other: &Self) -> bool {
328        self.0 == other.0
329    }
330}
331
332impl fmt::Display for SubnetId {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(f, "{}", self.0)
335    }
336}
337
338macro_rules! string_newtype {
339    ($name:ident) => {
340        #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
341        #[derive(Debug, Clone, Eq, PartialEq, Tag)]
342        #[tag(translate = transparent)]
343        pub struct $name(String);
344
345        impl std::fmt::Display for $name {
346            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347                write!(f, "{}", self.0)
348            }
349        }
350    };
351}
352
353string_newtype!(AvailabilityZone);
354
355#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
356#[derive(Debug, Clone)]
357pub struct Subnet {
358    pub id: SubnetId,
359    pub availability_zone: AvailabilityZone,
360}
361
362impl TryFrom<aws_sdk_ec2::types::Subnet> for Subnet {
363    type Error = Error;
364
365    fn try_from(subnet: aws_sdk_ec2::types::Subnet) -> Result<Self, Self::Error> {
366        macro_rules! extract {
367            ($field:ident) => {
368                subnet.$field.ok_or_else(|| Error::UnexpectedNoneValue {
369                    entity: stringify!($field).to_owned(),
370                })
371            };
372        }
373
374        Ok(Self {
375            id: SubnetId(extract!(subnet_id)?),
376            availability_zone: AvailabilityZone(extract!(availability_zone)?),
377        })
378    }
379}
380
381string_newtype!(InstanceId);
382
383impl InstanceId {
384    pub fn as_str(&self) -> &str {
385        &self.0
386    }
387}
388
389string_newtype!(AmiId);
390
391impl AmiId {
392    pub fn as_str(&self) -> &str {
393        &self.0
394    }
395}
396
397#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
398#[derive(Debug)]
399pub struct Ami {
400    pub id: AmiId,
401    pub tags: TagList,
402    pub creation_date: Timestamp,
403}
404
405impl TryFrom<aws_sdk_ec2::types::Image> for Ami {
406    type Error = Error;
407
408    fn try_from(image: aws_sdk_ec2::types::Image) -> Result<Self, Self::Error> {
409        macro_rules! extract {
410            ($field:ident) => {
411                image.$field.ok_or_else(|| Error::UnexpectedNoneValue {
412                    entity: stringify!($field).to_owned(),
413                })
414            };
415        }
416
417        Ok(Self {
418            id: AmiId(extract!(image_id)?),
419            tags: extract!(tags)?.try_into()?,
420            creation_date: RawImageCreationDate(extract!(creation_date)?).try_into()?,
421        })
422    }
423}
424
425#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
426#[derive(Tag, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
427#[tag(translate = manual)]
428pub struct Timestamp(DateTime<Utc>);
429
430impl Timestamp {
431    pub const fn new(value: DateTime<Utc>) -> Self {
432        Self(value)
433    }
434
435    pub const fn inner(&self) -> &DateTime<Utc> {
436        &self.0
437    }
438}
439
440impl fmt::Display for Timestamp {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        write!(f, "{}", self.0)
443    }
444}
445
446impl TryFrom<RawTagValue> for Timestamp {
447    type Error = ParseTagValueError;
448
449    fn try_from(value: RawTagValue) -> Result<Self, Self::Error> {
450        Ok(Self(
451            chrono::NaiveDateTime::parse_from_str(value.as_str(), "%Y-%m-%dT%H:%M:%S")
452                .map_err(|e| ParseTagValueError::InvalidValue {
453                    value,
454                    message: format!("failed parsing timestamp: {e}"),
455                })
456                .map(|timestamp| timestamp.and_utc())?,
457        ))
458    }
459}
460
461impl From<Timestamp> for RawTagValue {
462    fn from(value: Timestamp) -> Self {
463        Self::new(value.0.format("%Y-%m-%dT%H:%M:%S").to_string())
464    }
465}
466
467struct RawImageCreationDate(String);
468
469impl TryFrom<RawImageCreationDate> for Timestamp {
470    type Error = Error;
471
472    fn try_from(value: RawImageCreationDate) -> Result<Self, Self::Error> {
473        Ok(Self(
474            chrono::NaiveDateTime::parse_from_str(&value.0, "%Y-%m-%dT%H:%M:%S.%.3fZ")
475                .map_err(|e| Error::InvalidTimestampError {
476                    value: value.0,
477                    message: format!("failed parsing timestamp: {e}"),
478                })
479                .map(|timestamp| timestamp.and_utc())?,
480        ))
481    }
482}
483
484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[derive(Debug, Clone)]
486pub struct Ip(net::IpAddr);
487
488impl Ip {
489    pub const fn new(value: net::IpAddr) -> Self {
490        Self(value)
491    }
492
493    pub fn into_string(&self) -> String {
494        self.0.to_string()
495    }
496}
497
498impl fmt::Display for Ip {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        write!(f, "{}", self.0)
501    }
502}
503
504string_newtype!(EipAllocationId);
505
506impl EipAllocationId {
507    pub const fn new(value: String) -> Self {
508        Self(value)
509    }
510
511    pub fn as_str(&self) -> &str {
512        &self.0
513    }
514}
515
516#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
517#[derive(Debug)]
518pub struct Eip {
519    pub allocation_id: EipAllocationId,
520    pub ip: Ip,
521    pub associated_instance: Option<InstanceId>,
522}
523
524impl TryFrom<aws_sdk_ec2::types::Address> for Eip {
525    type Error = Error;
526
527    fn try_from(address: aws_sdk_ec2::types::Address) -> Result<Self, Self::Error> {
528        macro_rules! extract {
529            ($field:ident) => {
530                address.$field.ok_or_else(|| Error::UnexpectedNoneValue {
531                    entity: stringify!($field).to_owned(),
532                })
533            };
534        }
535
536        Ok(Self {
537            ip: Ip(extract!(public_ip)?.parse()?),
538            associated_instance: address.instance_id.map(InstanceId),
539            allocation_id: EipAllocationId(extract!(allocation_id)?),
540        })
541    }
542}
543
544impl Eip {
545    pub async fn attach_to_instance(
546        &self,
547        client: &RegionClient,
548        new_instance: &Instance,
549    ) -> Result<(), Error> {
550        let _association_id = client
551            .main
552            .ec2
553            .associate_address()
554            .allocation_id(self.allocation_id.as_str())
555            .instance_id(new_instance.instance_id().as_str())
556            .send()
557            .await?;
558
559        Ok(())
560    }
561
562    pub async fn set_tags(&self, client: &RegionClient, tags: TagList) -> Result<(), Error> {
563        let _output = client
564            .main
565            .ec2
566            .delete_tags()
567            .resources(self.allocation_id.as_str())
568            .send()
569            .await?;
570
571        let _output = client
572            .main
573            .ec2
574            .create_tags()
575            .resources(self.allocation_id.as_str())
576            .set_tags(Some(tags.into()))
577            .send()
578            .await?;
579
580        Ok(())
581    }
582}
583
584string_newtype!(CloudfrontDistributionId);
585
586#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
587#[derive(Debug, Clone)]
588#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
589pub enum CloudfrontDistributionStatus {
590    Deployed,
591    Other(String),
592}
593
594impl fmt::Display for CloudfrontDistributionStatus {
595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596        write!(
597            f,
598            "{}",
599            match *self {
600                Self::Deployed => "deployed",
601                Self::Other(ref s) => &s,
602            }
603        )
604    }
605}
606
607impl From<String> for CloudfrontDistributionStatus {
608    fn from(value: String) -> Self {
609        match value.as_str() {
610            "Deployed" => Self::Deployed,
611            _ => Self::Other(value),
612        }
613    }
614}
615
616#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
617#[derive(Debug, Clone)]
618pub struct EfsId(String);
619
620#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
621#[derive(Debug, Clone)]
622pub struct Efs {
623    id: EfsId,
624    region: Region,
625}
626
627impl Efs {
628    pub fn fs_dns_name(&self) -> String {
629        format!("{}.efs.{}.amazonaws.com", self.id.0, self.region.as_str())
630    }
631}
632
633impl TryFrom<(aws_sdk_efs::types::FileSystemDescription, Region)> for Efs {
634    type Error = Error;
635
636    fn try_from(
637        (efs, region): (aws_sdk_efs::types::FileSystemDescription, Region),
638    ) -> Result<Self, Self::Error> {
639        Ok(Self {
640            id: EfsId(efs.file_system_id),
641            region,
642        })
643    }
644}
645
646string_newtype!(CloudfrontDistributionDomain);
647
648impl From<String> for CloudfrontDistributionDomain {
649    fn from(value: String) -> Self {
650        Self(value)
651    }
652}
653
654#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
655#[derive(Debug, Clone)]
656pub struct CloudfrontOrigin {
657    id: CloudfrontOriginId,
658    domain: CloudfrontOriginDomain,
659}
660
661impl CloudfrontOrigin {
662    pub const fn id(&self) -> &CloudfrontOriginId {
663        &self.id
664    }
665
666    pub const fn domain(&self) -> &CloudfrontOriginDomain {
667        &self.domain
668    }
669}
670
671string_newtype!(CloudfrontOriginId);
672string_newtype!(CloudfrontOriginDomain);
673
674impl PartialEq<str> for CloudfrontOriginId {
675    fn eq(&self, other: &str) -> bool {
676        self.0 == other
677    }
678}
679
680impl CloudfrontOriginDomain {
681    pub fn as_str(&self) -> &str {
682        &self.0
683    }
684}
685
686impl From<String> for CloudfrontOriginDomain {
687    fn from(value: String) -> Self {
688        Self(value)
689    }
690}
691
692impl From<aws_sdk_cloudfront::types::Origin> for CloudfrontOrigin {
693    fn from(value: aws_sdk_cloudfront::types::Origin) -> Self {
694        Self {
695            id: CloudfrontOriginId(value.id),
696            domain: value.domain_name.into(),
697        }
698    }
699}
700
701#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
702#[derive(Debug, Clone)]
703pub struct CloudfrontDistribution {
704    pub id: CloudfrontDistributionId,
705    pub status: CloudfrontDistributionStatus,
706    pub domain: CloudfrontDistributionDomain,
707    pub origins: Vec<CloudfrontOrigin>,
708}
709
710impl TryFrom<aws_sdk_cloudfront::types::DistributionSummary> for CloudfrontDistribution {
711    type Error = Error;
712
713    fn try_from(
714        distribution: aws_sdk_cloudfront::types::DistributionSummary,
715    ) -> Result<Self, Self::Error> {
716        Ok(Self {
717            id: CloudfrontDistributionId(distribution.id),
718            status: distribution.status.into(),
719            domain: distribution.domain_name.into(),
720            origins: distribution.origins.map_or_else(Vec::new, |origins| {
721                origins.items.into_iter().map(Into::into).collect()
722            }),
723        })
724    }
725}
726
727impl CloudfrontDistribution {
728    pub fn origins(&self) -> &[CloudfrontOrigin] {
729        &self.origins
730    }
731
732    pub const fn domain(&self) -> &CloudfrontDistributionDomain {
733        &self.domain
734    }
735
736    pub const fn status(&self) -> &CloudfrontDistributionStatus {
737        &self.status
738    }
739}
740
741#[derive(Clone)]
742pub struct ProfileName(String);
743
744impl ProfileName {
745    pub const fn new(value: String) -> Self {
746        Self(value)
747    }
748}
749
750#[derive(Clone)]
751pub struct ProfileConfig {
752    pub profile_name_main: ProfileName,
753    pub profile_name_cdn: ProfileName,
754}
755
756pub async fn load_sdk_clients<const C: usize>(
757    regions: [Region; C],
758    profile_config: ProfileConfig,
759) -> Vec<RegionClient> {
760    let mut region_clients = vec![];
761
762    for region in regions {
763        let base_config = || {
764            aws_config::ConfigLoader::default()
765                .retry_config(RetryConfig::standard())
766                .stalled_stream_protection(
767                    aws_sdk_ec2::config::StalledStreamProtectionConfig::enabled()
768                        .grace_period(Duration::from_secs(5))
769                        .build(),
770                )
771                .behavior_version(aws_config::BehaviorVersion::latest())
772        };
773
774        let config = base_config()
775            .profile_name(&profile_config.profile_name_main.0)
776            .region(region.name())
777            .load()
778            .await;
779
780        let config_cdn = base_config()
781            .profile_name(&profile_config.profile_name_cdn.0)
782            .region(region.name())
783            .load()
784            .await;
785
786        // Cloudformation needs always be run in us-east-1
787        let config_cloudformation = base_config()
788            .profile_name(&profile_config.profile_name_cdn.0)
789            .region(Region::UsEast1.as_str())
790            .load()
791            .await;
792
793        let ec2_client = aws_sdk_ec2::Client::new(&config);
794        let cloudfront_client = aws_sdk_cloudfront::Client::new(&config_cdn);
795        let efs_client = aws_sdk_efs::Client::new(&config);
796        let route53_client = aws_sdk_route53::Client::new(&config);
797        let cloudformation_client = aws_sdk_cloudformation::Client::new(&config_cloudformation);
798
799        region_clients.push(RegionClient {
800            region,
801            main: RegionClientMain {
802                ec2: ec2_client,
803                efs: efs_client,
804                route53: route53_client,
805            },
806            cdn: RegionClientCdn {
807                cloudfront: cloudfront_client,
808                cloudformation: cloudformation_client,
809            },
810        });
811    }
812
813    region_clients
814}
815
816#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
817#[derive(Debug, Clone)]
818pub struct Account {
819    id: String,
820}
821
822impl Account {
823    pub const fn new(id: String) -> Self {
824        Self { id }
825    }
826
827    pub fn id(&self) -> &str {
828        &self.id
829    }
830}
831
832#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
833#[derive(Debug, Clone)]
834pub struct HostedZoneId(String);
835
836impl HostedZoneId {
837    pub const fn new(value: String) -> Self {
838        Self(value)
839    }
840
841    pub fn as_str(&self) -> &str {
842        &self.0
843    }
844}
845
846#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
847#[derive(Debug, Clone)]
848pub struct Route53Zone {
849    hosted_zone_id: HostedZoneId,
850    name: String,
851}
852
853impl Route53Zone {
854    pub async fn find_by_name(client: &RegionClient, name: &str) -> Result<Option<Self>, Error> {
855        Ok(client
856            .main
857            .route53
858            .list_hosted_zones()
859            .into_paginator()
860            .items()
861            .send()
862            .try_collect()
863            .await?
864            .into_iter()
865            .filter(|zone| zone.name == name)
866            .map(Into::into)
867            .next())
868    }
869
870    pub const fn new(name: String, hosted_zone_id: HostedZoneId) -> Self {
871        Self {
872            hosted_zone_id,
873            name,
874        }
875    }
876
877    pub const fn hosted_zone_id(&self) -> &HostedZoneId {
878        &self.hosted_zone_id
879    }
880
881    pub fn name(&self) -> &str {
882        &self.name
883    }
884}
885
886impl From<aws_sdk_route53::types::HostedZone> for Route53Zone {
887    fn from(zone: aws_sdk_route53::types::HostedZone) -> Self {
888        Self {
889            hosted_zone_id: HostedZoneId(zone.id),
890            name: zone.name,
891        }
892    }
893}
894
895pub struct NewEc2Config<'a> {
896    pub ami: &'a Ami,
897    pub instance_type: &'a InstanceType,
898    pub security_group: &'a SecurityGroup,
899    pub instance_profile_name: &'a InstanceProfileName,
900    pub instance_keypair_name: &'a InstanceKeypairName,
901    pub subnet_id: &'a SubnetId,
902    pub user_data: &'a str,
903    pub tags: &'a TagList,
904}
905
906pub async fn start_ec2_instance<'a>(
907    client: &RegionClient,
908    ami: &'a Ami,
909    instance_type: &'a InstanceType,
910    security_group: &'a SecurityGroup,
911    instance_profile_name: &'a InstanceProfileName,
912    instance_keypair_name: &'a InstanceKeypairName,
913    subnet_id: &'a SubnetId,
914    user_data: &'a str,
915    tags: &'a TagList,
916) -> Result<Instance, Error> {
917    Instance::try_from_aws(
918        client
919            .main
920            .ec2
921            .run_instances()
922            .image_id(ami.id.as_str())
923            .instance_type(instance_type.clone().into_inner())
924            .key_name(instance_keypair_name.as_str())
925            .min_count(1)
926            .max_count(1)
927            .security_group_ids(security_group.id.as_str())
928            .subnet_id(subnet_id.as_str())
929            .user_data(user_data)
930            .tag_specifications(
931                aws_sdk_ec2::types::TagSpecification::builder()
932                    .resource_type(aws_sdk_ec2::types::ResourceType::Instance)
933                    .set_tags(Some(tags.clone().into()))
934                    .build(),
935            )
936            .metadata_options(
937                aws_sdk_ec2::types::InstanceMetadataOptionsRequest::builder()
938                    .http_tokens(aws_sdk_ec2::types::HttpTokensState::Optional)
939                    .http_endpoint(aws_sdk_ec2::types::InstanceMetadataEndpointState::Enabled)
940                    .instance_metadata_tags(aws_sdk_ec2::types::InstanceMetadataTagsState::Enabled)
941                    .build(),
942            )
943            .disable_api_termination(true)
944            .iam_instance_profile(
945                aws_sdk_ec2::types::IamInstanceProfileSpecification::builder()
946                    .name(instance_profile_name.as_str())
947                    .build(),
948            )
949            .send()
950            .await?
951            .instances
952            .ok_or(Error::UnexpectedNoneValue {
953                entity: "RunInstancesOutput.instances".to_owned(),
954            })?
955            .pop()
956            .ok_or(Error::RunInstancesEmptyResponse)?,
957    )
958}
959
960pub async fn create_cloudformation_stack(
961    client: &RegionClient,
962    name: &str,
963    template: &str,
964    parameters: &CloudformationParameters,
965    tags: &TagList,
966) -> Result<(), Error> {
967    let _create_stack_output = client
968        .cdn
969        .cloudformation
970        .create_stack()
971        .stack_name(name)
972        .template_body(template)
973        .set_parameters(Some(
974            parameters
975                .0
976                .iter()
977                .map(|param| {
978                    aws_sdk_cloudformation::types::Parameter::builder()
979                        .parameter_key(param.key.as_str())
980                        .parameter_value(param.value.as_str())
981                        .build()
982                })
983                .collect(),
984        ))
985        .disable_rollback(true)
986        .capabilities(aws_sdk_cloudformation::types::Capability::CapabilityAutoExpand)
987        .set_tags(Some(tags.clone().into()))
988        .send()
989        .await?;
990
991    Ok(())
992}
993
994pub struct CloudformationParameter {
995    key: String,
996    value: String,
997}
998
999impl CloudformationParameter {
1000    pub const fn new(key: String, value: String) -> Self {
1001        Self { key, value }
1002    }
1003}
1004
1005pub struct CloudformationParameters(Vec<CloudformationParameter>);
1006
1007impl CloudformationParameters {
1008    pub const fn new(value: Vec<CloudformationParameter>) -> Self {
1009        Self(value)
1010    }
1011}
1012
1013#[expect(
1014    clippy::missing_panics_doc,
1015    reason = "only expect() on builder instances"
1016)]
1017pub async fn create_route53_record(
1018    client: &RegionClient,
1019    eip: &Eip,
1020    route53_zone: &Route53Zone,
1021    fqdn: &str,
1022) -> Result<(), Error> {
1023    let _change_info = client
1024        .main
1025        .route53
1026        .change_resource_record_sets()
1027        .hosted_zone_id(route53_zone.hosted_zone_id.as_str())
1028        .change_batch(
1029            aws_sdk_route53::types::ChangeBatch::builder()
1030                .changes(
1031                    aws_sdk_route53::types::Change::builder()
1032                        .action(aws_sdk_route53::types::ChangeAction::Create)
1033                        .resource_record_set(
1034                            aws_sdk_route53::types::ResourceRecordSet::builder()
1035                                .name(fqdn)
1036                                .r#type(aws_sdk_route53::types::RrType::A)
1037                                .ttl(600)
1038                                .resource_records(
1039                                    aws_sdk_route53::types::ResourceRecord::builder()
1040                                        .value(eip.ip.to_string())
1041                                        .build()
1042                                        .expect("builder has missing fields"),
1043                                )
1044                                .build()
1045                                .expect("builder has missing fields"),
1046                        )
1047                        .build()
1048                        .expect("builder has missing fields"),
1049                )
1050                .build()
1051                .expect("builder has missing fields"),
1052        )
1053        .send()
1054        .await?;
1055
1056    Ok(())
1057}
1058
1059pub async fn find_efs(client: &RegionClient, tag: &RawTag) -> Result<Option<Efs>, Error> {
1060    let mut found = client
1061        .main
1062        .efs
1063        .describe_file_systems()
1064        .into_paginator()
1065        .items()
1066        .send()
1067        .try_collect()
1068        .await?
1069        .into_iter()
1070        .filter(|fs| fs.tags.iter().any(|t| t == tag))
1071        .map(|fs| (fs, client.region).try_into())
1072        .collect::<Result<Vec<Efs>, Error>>()?;
1073
1074    match (found.len(), found.pop()) {
1075        (0, _) => Ok(None),
1076        (1, Some(found)) => Ok(Some(found)),
1077        _ => Err(Error::MultipleMatches {
1078            entity: "efs".to_owned(),
1079        }),
1080    }
1081}