use super::instance_mapper::AwsInstanceMapper;
use crate::core::error::{Error, Result};
use crate::core::resources::ResourceSpec;
use crate::providers::common::{ProvisionedInfrastructure, ProvisioningConfig};
#[cfg(feature = "aws")]
use aws_sdk_ec2::types::{
EnclaveOptionsRequest, InstanceType, ResourceType, Tag, TagSpecification,
};
use blueprint_core::{info, warn};
pub struct AwsProvisioner {
pub(crate) ec2_client: aws_sdk_ec2::Client,
#[cfg(feature = "aws-eks")]
pub(crate) eks_client: Option<aws_sdk_eks::Client>,
}
impl AwsProvisioner {
pub async fn new() -> Result<Self> {
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let ec2_client = aws_sdk_ec2::Client::new(&config);
#[cfg(feature = "aws-eks")]
let eks_client = Some(aws_sdk_eks::Client::new(&config));
Ok(Self {
ec2_client,
#[cfg(feature = "aws-eks")]
eks_client,
})
}
pub async fn provision_instance(
&self,
spec: &ResourceSpec,
config: &ProvisioningConfig,
) -> Result<ProvisionedInfrastructure> {
let mut instance_selection = AwsInstanceMapper::map(spec);
if let Some(override_type) = config.custom_config.get("instance_type") {
instance_selection.instance_type = override_type.clone();
}
let require_tee = config
.custom_config
.get("require_tee")
.is_some_and(|value| value.eq_ignore_ascii_case("true"));
let mut request = self
.ec2_client
.run_instances()
.image_id(config.ami_id.as_deref().unwrap_or("ami-0c55b159cbfafe1f0")) .instance_type(InstanceType::from(
instance_selection.instance_type.as_str(),
))
.min_count(1)
.max_count(1)
.key_name(config.ssh_key_name.as_deref().unwrap_or("default"))
.tag_specifications(
TagSpecification::builder()
.resource_type(ResourceType::Instance)
.tags(Tag::builder().key("Name").value(&config.name).build())
.tags(
Tag::builder()
.key("BlueprintDeployment")
.value("true")
.build(),
)
.tags(
Tag::builder()
.key("Provider")
.value("blueprint-remote-providers")
.build(),
)
.build(),
);
if require_tee {
request =
request.enclave_options(EnclaveOptionsRequest::builder().enabled(true).build());
}
let result = request.send().await?;
let instance = result
.instances()
.first()
.ok_or_else(|| Error::ConfigurationError("No instance created".into()))?;
let instance_id = instance
.instance_id()
.ok_or_else(|| Error::ConfigurationError("No instance ID".into()))?;
info!("Created AWS EC2 instance: {}", instance_id);
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
let describe_result = self
.ec2_client
.describe_instances()
.instance_ids(instance_id)
.send()
.await?;
let reservation = describe_result
.reservations()
.first()
.ok_or_else(|| Error::ConfigurationError("No reservation found".into()))?;
let instance = reservation
.instances()
.first()
.ok_or_else(|| Error::ConfigurationError("No instance found".into()))?;
let public_ip = instance.public_ip_address().map(|s| s.to_string());
let private_ip = instance.private_ip_address().map(|s| s.to_string());
Ok(ProvisionedInfrastructure {
provider: crate::core::remote::CloudProvider::AWS,
instance_id: instance_id.to_string(),
public_ip,
private_ip,
region: config.region.clone(),
instance_type: instance_selection.instance_type,
metadata: {
let mut metadata = std::collections::HashMap::new();
metadata.insert("require_tee".to_string(), require_tee.to_string());
metadata
},
})
}
pub async fn terminate_instance(&self, instance_id: &str) -> Result<()> {
self.ec2_client
.terminate_instances()
.instance_ids(instance_id)
.send()
.await?;
info!("Terminated AWS EC2 instance: {}", instance_id);
Ok(())
}
pub async fn get_instance_status(
&self,
instance_id: &str,
) -> Result<crate::infra::types::InstanceStatus> {
let describe_result = self
.ec2_client
.describe_instances()
.instance_ids(instance_id)
.send()
.await
.map_err(|e| Error::ConfigurationError(format!("Failed to describe instance: {e}")))?;
let instance = describe_result
.reservations()
.first()
.and_then(|r| r.instances().first())
.ok_or_else(|| Error::ConfigurationError("Instance not found".into()))?;
let state_name = instance
.state()
.and_then(|s| s.name())
.map(|n| format!("{n:?}"))
.unwrap_or_else(|| "unknown".to_string());
match state_name.to_lowercase().as_str() {
"running" => Ok(crate::infra::types::InstanceStatus::Running),
"pending" => Ok(crate::infra::types::InstanceStatus::Starting),
"stopping" | "stopped" | "terminated" => {
Ok(crate::infra::types::InstanceStatus::Terminated)
}
_ => Ok(crate::infra::types::InstanceStatus::Unknown),
}
}
pub async fn create_security_group(&self, sg_name: &str) -> Result<String> {
use aws_sdk_ec2::types::{IpPermission, IpRange};
let create_result = self
.ec2_client
.create_security_group()
.group_name(sg_name)
.description("Blueprint remote providers security group - SSH and QoS ports")
.send()
.await
.map_err(|e| {
Error::ConfigurationError(format!("Failed to create security group: {e}"))
})?;
let sg_id = create_result.group_id().unwrap_or("").to_string();
let ssh_rule = IpPermission::builder()
.ip_protocol("tcp")
.from_port(22)
.to_port(22)
.ip_ranges(IpRange::builder().cidr_ip("0.0.0.0/0").build())
.build();
let qos_rule = IpPermission::builder()
.ip_protocol("tcp")
.from_port(8080)
.to_port(9944)
.ip_ranges(IpRange::builder().cidr_ip("0.0.0.0/0").build())
.build();
match self
.ec2_client
.authorize_security_group_ingress()
.group_id(&sg_id)
.ip_permissions(ssh_rule)
.ip_permissions(qos_rule)
.send()
.await
{
Ok(_) => info!("Security group {} configured with ingress rules", sg_id),
Err(e) => warn!("Failed to configure security group rules: {}", e),
}
Ok(sg_id)
}
}