1extern 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() .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 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}