use crate::{
error::{Error, Result},
print_duration,
terraform::{TerraformResource, TerraformRunner},
DeploymentType, EnvironmentDetails, TestnetDeployer,
};
use std::time::Instant;
#[derive(Clone, Debug)]
pub struct InfraRunOptions {
pub enable_build_vm: bool,
pub evm_node_count: Option<u16>,
pub evm_node_vm_size: Option<String>,
pub genesis_vm_count: Option<u16>,
pub genesis_node_volume_size: Option<u16>,
pub name: String,
pub node_vm_count: Option<u16>,
pub node_vm_size: Option<String>,
pub node_volume_size: Option<u16>,
pub peer_cache_node_vm_count: Option<u16>,
pub peer_cache_node_vm_size: Option<String>,
pub peer_cache_node_volume_size: Option<u16>,
pub private_node_vm_count: Option<u16>,
pub private_node_volume_size: Option<u16>,
pub setup_nat_gateway: bool,
pub tfvars_filename: String,
pub uploader_vm_count: Option<u16>,
pub uploader_vm_size: Option<String>,
}
impl InfraRunOptions {
fn get_value_for_resource(
resources: &[TerraformResource],
resource_name: &str,
field_name: &str,
) -> Result<serde_json::Value, Error> {
let field_value = resources
.iter()
.filter(|r| r.resource_name == resource_name)
.try_fold(None, |acc_value: Option<serde_json::Value>, r| {
let Some(value) = r.values.get(field_name) else {
log::error!("Failed to obtain '{field_name}' value for {resource_name}");
return Err(Error::TerraformResourceFieldMissing(field_name.to_string()));
};
match acc_value {
Some(ref existing_value) if existing_value != value => {
log::error!("Expected value: {existing_value}, got value: {value}");
Err(Error::TerraformResourceValueMismatch {
expected: existing_value.to_string(),
actual: value.to_string(),
})
}
_ => Ok(Some(value.clone())),
}
})?;
field_value.ok_or(Error::TerraformResourceFieldMissing(field_name.to_string()))
}
pub async fn generate_existing(
name: &str,
terraform_runner: &TerraformRunner,
environment_details: &EnvironmentDetails,
) -> Result<Self> {
let resources = terraform_runner.show(name)?;
let resource_count = |resource_name: &str| -> u16 {
resources
.iter()
.filter(|r| r.resource_name == resource_name)
.count() as u16
};
let peer_cache_node_vm_count = resource_count("peer_cache_node");
let peer_cache_node_volume_size = if peer_cache_node_vm_count > 0 {
let volume_size = Self::get_value_for_resource(
&resources,
"peer_cache_node_attached_volume",
"size",
)?
.as_u64()
.ok_or_else(|| {
log::error!(
"Failed to obtain u64 'size' value for peer_cache_node_attached_volume"
);
Error::TerraformResourceFieldMissing("size".to_string())
})?;
Some(volume_size as u16)
} else {
None
};
let peer_cache_node_vm_size =
Self::get_value_for_resource(&resources, "peer_cache_node", "size")?;
let peer_cache_node_vm_size = peer_cache_node_vm_size.as_str().ok_or_else(|| {
log::error!("Failed to obtain str 'size' value for peer_cache_node");
Error::TerraformResourceFieldMissing("size".to_string())
})?;
let genesis_vm_count = match environment_details.deployment_type {
DeploymentType::New => 1,
DeploymentType::Bootstrap => 0,
};
let genesis_node_volume_size = if genesis_vm_count > 0 {
let genesis_node_volume_size =
Self::get_value_for_resource(&resources, "genesis_node_attached_volume", "size")?
.as_u64()
.ok_or_else(|| {
log::error!(
"Failed to obtain u64 'size' value for genesis_node_attached_volume"
);
Error::TerraformResourceFieldMissing("size".to_string())
})?;
Some(genesis_node_volume_size as u16)
} else {
None
};
let node_vm_count = resource_count("node");
let node_volume_size = if node_vm_count > 0 {
let node_volume_size =
Self::get_value_for_resource(&resources, "node_attached_volume", "size")?
.as_u64()
.ok_or_else(|| {
log::error!("Failed to obtain u64 'size' value for node_attached_volume");
Error::TerraformResourceFieldMissing("size".to_string())
})?;
Some(node_volume_size as u16)
} else {
None
};
let node_vm_size = Self::get_value_for_resource(&resources, "node", "size")?;
let node_vm_size = node_vm_size.as_str().ok_or_else(|| {
log::error!("Failed to obtain str 'size' value for node");
Error::TerraformResourceFieldMissing("size".to_string())
})?;
let private_node_vm_count = resource_count("private_node");
let private_node_volume_size = if private_node_vm_count > 0 {
let private_node_volume_size =
Self::get_value_for_resource(&resources, "private_node_attached_volume", "size")?
.as_u64()
.ok_or_else(|| {
log::error!(
"Failed to obtain u64 'size' value for private_node_attached_volume"
);
Error::TerraformResourceFieldMissing("size".to_string())
})?;
Some(private_node_volume_size as u16)
} else {
None
};
let uploader_vm_count = Some(resource_count("uploader"));
let uploader_vm_size = Self::get_value_for_resource(&resources, "uploader", "size")?;
let uploader_vm_size = uploader_vm_size.as_str().ok_or_else(|| {
log::error!("Failed to obtain str 'size' value for uploader");
Error::TerraformResourceFieldMissing("size".to_string())
})?;
let evm_node_count = Some(resource_count("evm_node"));
let build_vm_count = resource_count("build");
let enable_build_vm = build_vm_count > 0;
let setup_nat_gateway = private_node_vm_count > 0;
let options = Self {
enable_build_vm,
evm_node_count,
evm_node_vm_size: None,
genesis_vm_count: Some(genesis_vm_count),
genesis_node_volume_size,
name: name.to_string(),
node_vm_count: Some(node_vm_count),
node_vm_size: Some(node_vm_size.to_string()),
node_volume_size,
peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
peer_cache_node_vm_size: Some(peer_cache_node_vm_size.to_string()),
peer_cache_node_volume_size,
private_node_vm_count: Some(private_node_vm_count),
private_node_volume_size,
setup_nat_gateway,
tfvars_filename: environment_details
.environment_type
.get_tfvars_filename(name),
uploader_vm_count,
uploader_vm_size: Some(uploader_vm_size.to_string()),
};
Ok(options)
}
}
impl TestnetDeployer {
pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
let start = Instant::now();
println!("Selecting {} workspace...", options.name);
self.terraform_runner.workspace_select(&options.name)?;
let args = build_terraform_args(options)?;
println!("Running terraform apply...");
self.terraform_runner
.apply(args, Some(options.tfvars_filename.clone()))?;
print_duration(start.elapsed());
Ok(())
}
}
pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
let mut args = Vec::new();
if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
}
if let Some(genesis_vm_count) = options.genesis_vm_count {
args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
}
if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
args.push((
"peer_cache_node_vm_count".to_string(),
peer_cache_node_vm_count.to_string(),
));
}
if let Some(node_vm_count) = options.node_vm_count {
args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
}
args.push((
"setup_nat_gateway".to_string(),
options.setup_nat_gateway.to_string(),
));
if let Some(private_node_vm_count) = options.private_node_vm_count {
args.push((
"private_node_vm_count".to_string(),
private_node_vm_count.to_string(),
));
}
if let Some(evm_node_count) = options.evm_node_count {
args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
}
if let Some(uploader_vm_count) = options.uploader_vm_count {
args.push((
"uploader_vm_count".to_string(),
uploader_vm_count.to_string(),
));
}
args.push((
"use_custom_bin".to_string(),
options.enable_build_vm.to_string(),
));
if let Some(node_vm_size) = &options.node_vm_size {
args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
}
if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
args.push((
"peer_cache_droplet_size".to_string(),
peer_cache_vm_size.clone(),
));
}
if let Some(uploader_vm_size) = &options.uploader_vm_size {
args.push((
"uploader_droplet_size".to_string(),
uploader_vm_size.clone(),
));
}
if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
args.push((
"evm_node_droplet_size".to_string(),
evm_node_vm_size.clone(),
));
}
if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
args.push((
"peer_cache_node_volume_size".to_string(),
peer_cache_node_volume_size.to_string(),
));
}
if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
args.push((
"genesis_node_volume_size".to_string(),
genesis_node_volume_size.to_string(),
));
}
if let Some(node_volume_size) = options.node_volume_size {
args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
}
if let Some(private_node_volume_size) = options.private_node_volume_size {
args.push((
"private_node_volume_size".to_string(),
private_node_volume_size.to_string(),
));
}
Ok(args)
}