1use crate::{
8 error::{Error, Result},
9 print_duration,
10 terraform::{TerraformResource, TerraformRunner},
11 DeploymentType, EnvironmentDetails, TestnetDeployer,
12};
13use std::time::Instant;
14
15#[derive(Clone, Debug)]
16pub struct InfraRunOptions {
17 pub enable_build_vm: bool,
18 pub evm_node_count: Option<u16>,
19 pub evm_node_vm_size: Option<String>,
20 pub full_cone_nat_gateway_vm_size: Option<String>,
21 pub full_cone_private_node_vm_count: Option<u16>,
22 pub full_cone_private_node_volume_size: Option<u16>,
23 pub genesis_vm_count: Option<u16>,
24 pub genesis_node_volume_size: Option<u16>,
25 pub name: String,
26 pub node_vm_count: Option<u16>,
27 pub node_vm_size: Option<String>,
28 pub node_volume_size: Option<u16>,
29 pub peer_cache_node_vm_count: Option<u16>,
30 pub peer_cache_node_vm_size: Option<String>,
31 pub peer_cache_node_volume_size: Option<u16>,
32 pub symmetric_nat_gateway_vm_size: Option<String>,
33 pub symmetric_private_node_vm_count: Option<u16>,
34 pub symmetric_private_node_volume_size: Option<u16>,
35 pub tfvars_filename: String,
36 pub uploader_vm_count: Option<u16>,
37 pub uploader_vm_size: Option<String>,
38}
39
40impl InfraRunOptions {
41 fn get_value_for_resource(
42 resources: &[TerraformResource],
43 resource_name: &str,
44 field_name: &str,
45 ) -> Result<serde_json::Value, Error> {
46 let field_value = resources
47 .iter()
48 .filter(|r| r.resource_name == resource_name)
49 .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
50 let Some(value) = r.values.get(field_name) else {
51 log::error!("Failed to obtain '{field_name}' value for {resource_name}");
52 return Err(Error::TerraformResourceFieldMissing(field_name.to_string()));
53 };
54 match acc_value {
55 Some(ref existing_value) if existing_value != value => {
56 log::error!("Expected value: {existing_value}, got value: {value}");
57 Err(Error::TerraformResourceValueMismatch {
58 expected: existing_value.to_string(),
59 actual: value.to_string(),
60 })
61 }
62 _ => Ok(Some(value.clone())),
63 }
64 })?;
65
66 field_value.ok_or(Error::TerraformResourceFieldMissing(field_name.to_string()))
67 }
68
69 pub async fn generate_existing(
71 name: &str,
72 terraform_runner: &TerraformRunner,
73 environment_details: &EnvironmentDetails,
74 ) -> Result<Self> {
75 let resources = terraform_runner.show(name)?;
76
77 let resource_count = |resource_name: &str| -> u16 {
78 resources
79 .iter()
80 .filter(|r| r.resource_name == resource_name)
81 .count() as u16
82 };
83
84 let peer_cache_node_vm_count = resource_count("peer_cache_node");
85 let (peer_cache_node_volume_size, peer_cache_node_vm_size) = if peer_cache_node_vm_count > 0
86 {
87 let volume_size = Self::get_value_for_resource(
88 &resources,
89 "peer_cache_node_attached_volume",
90 "size",
91 )?
92 .as_u64()
93 .ok_or_else(|| {
94 log::error!(
95 "Failed to obtain u64 'size' value for peer_cache_node_attached_volume"
96 );
97 Error::TerraformResourceFieldMissing("size".to_string())
98 })?;
99 let vm_size = Self::get_value_for_resource(&resources, "peer_cache_node", "size")?
100 .as_str()
101 .ok_or_else(|| {
102 log::error!("Failed to obtain str 'size' value for peer_cache_node");
103 Error::TerraformResourceFieldMissing("size".to_string())
104 })?
105 .to_string();
106
107 (Some(volume_size as u16), Some(vm_size))
108 } else {
109 (None, None)
110 };
111
112 let genesis_vm_count = match environment_details.deployment_type {
114 DeploymentType::New => 1,
115 DeploymentType::Bootstrap => 0,
116 };
117 let genesis_node_volume_size = if genesis_vm_count > 0 {
118 let genesis_node_volume_size =
119 Self::get_value_for_resource(&resources, "genesis_node_attached_volume", "size")?
120 .as_u64()
121 .ok_or_else(|| {
122 log::error!(
123 "Failed to obtain u64 'size' value for genesis_node_attached_volume"
124 );
125 Error::TerraformResourceFieldMissing("size".to_string())
126 })?;
127 Some(genesis_node_volume_size as u16)
128 } else {
129 None
130 };
131
132 let node_vm_count = resource_count("node");
133 let node_volume_size = if node_vm_count > 0 {
134 let node_volume_size =
135 Self::get_value_for_resource(&resources, "node_attached_volume", "size")?
136 .as_u64()
137 .ok_or_else(|| {
138 log::error!("Failed to obtain u64 'size' value for node_attached_volume");
139 Error::TerraformResourceFieldMissing("size".to_string())
140 })?;
141
142 Some(node_volume_size as u16)
143 } else {
144 None
145 };
146
147 let symmetric_private_node_vm_count = resource_count("symmetric_private_node");
148 let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
149 if symmetric_private_node_vm_count > 0 {
150 let symmetric_private_node_volume_size = Self::get_value_for_resource(
151 &resources,
152 "symmetric_private_node_attached_volume",
153 "size",
154 )?
155 .as_u64()
156 .ok_or_else(|| {
157 log::error!(
158 "Failed to obtain u64 'size' value for symmetric_private_node_attached_volume"
159 );
160 Error::TerraformResourceFieldMissing("size".to_string())
161 })?;
162 let symmetric_nat_gateway_vm_size =
164 Self::get_value_for_resource(&resources, "symmetric_nat_gateway", "size")?
165 .as_str()
166 .ok_or_else(|| {
167 log::error!(
168 "Failed to obtain str 'size' value for symmetric_nat_gateway"
169 );
170 Error::TerraformResourceFieldMissing("size".to_string())
171 })?
172 .to_string();
173
174 (
175 Some(symmetric_private_node_volume_size as u16),
176 Some(symmetric_nat_gateway_vm_size),
177 )
178 } else {
179 (None, None)
180 };
181 let full_cone_private_node_vm_count = resource_count("full_cone_private_node");
182 let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
183 if full_cone_private_node_vm_count > 0 {
184 let full_cone_private_node_volume_size = Self::get_value_for_resource(
185 &resources,
186 "full_cone_private_node_attached_volume",
187 "size",
188 )?
189 .as_u64()
190 .ok_or_else(|| {
191 log::error!(
192 "Failed to obtain u64 'size' value for full_cone_private_node_attached_volume"
193 );
194 Error::TerraformResourceFieldMissing("size".to_string())
195 })?;
196 let full_cone_nat_gateway_vm_size =
198 Self::get_value_for_resource(&resources, "full_cone_nat_gateway", "size")?
199 .as_str()
200 .ok_or_else(|| {
201 log::error!(
202 "Failed to obtain str 'size' value for full_cone_nat_gateway"
203 );
204 Error::TerraformResourceFieldMissing("size".to_string())
205 })?
206 .to_string();
207
208 (
209 Some(full_cone_private_node_volume_size as u16),
210 Some(full_cone_nat_gateway_vm_size),
211 )
212 } else {
213 (None, None)
214 };
215
216 let uploader_vm_count = resource_count("uploader");
217 let uploader_vm_size = if uploader_vm_count > 0 {
218 let uploader_vm_size = Self::get_value_for_resource(&resources, "uploader", "size")?
219 .as_str()
220 .ok_or_else(|| {
221 log::error!("Failed to obtain str 'size' value for uploader");
222 Error::TerraformResourceFieldMissing("size".to_string())
223 })?
224 .to_string();
225 Some(uploader_vm_size)
226 } else {
227 None
228 };
229
230 let evm_node_count = resource_count("evm_node");
231 let build_vm_count = resource_count("build");
232 let enable_build_vm = build_vm_count > 0;
233
234 let node_vm_size = if node_vm_count > 0 {
236 let node_vm_size = Self::get_value_for_resource(&resources, "node", "size")?
237 .as_str()
238 .ok_or_else(|| {
239 log::error!("Failed to obtain str 'size' value for node");
240 Error::TerraformResourceFieldMissing("size".to_string())
241 })?
242 .to_string();
243 Some(node_vm_size)
244 } else if symmetric_private_node_vm_count > 0 {
245 let symmetric_private_node_vm_size =
246 Self::get_value_for_resource(&resources, "symmetric_private_node", "size")?
247 .as_str()
248 .ok_or_else(|| {
249 log::error!("Failed to obtain str 'size' value for symmetric_private_node");
250 Error::TerraformResourceFieldMissing("size".to_string())
251 })?
252 .to_string();
253 Some(symmetric_private_node_vm_size)
254 } else if full_cone_private_node_vm_count > 0 {
255 let full_cone_private_node_vm_size =
256 Self::get_value_for_resource(&resources, "full_cone_private_node", "size")?
257 .as_str()
258 .ok_or_else(|| {
259 log::error!("Failed to obtain str 'size' value for full_cone_private_node");
260 Error::TerraformResourceFieldMissing("size".to_string())
261 })?
262 .to_string();
263 Some(full_cone_private_node_vm_size)
264 } else if evm_node_count > 0 {
265 let evm_node_vm_size = Self::get_value_for_resource(&resources, "evm_node", "size")?
266 .as_str()
267 .ok_or_else(|| {
268 log::error!("Failed to obtain str 'size' value for evm_node");
269 Error::TerraformResourceFieldMissing("size".to_string())
270 })?
271 .to_string();
272 Some(evm_node_vm_size)
273 } else {
274 None
275 };
276
277 let options = Self {
278 enable_build_vm,
279 evm_node_count: Some(evm_node_count),
280 evm_node_vm_size: None,
282 full_cone_nat_gateway_vm_size,
283 full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
284 full_cone_private_node_volume_size,
285 genesis_vm_count: Some(genesis_vm_count),
286 genesis_node_volume_size,
287 name: name.to_string(),
288 node_vm_count: Some(node_vm_count),
289 node_vm_size,
290 node_volume_size,
291 peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
292 peer_cache_node_vm_size,
293 peer_cache_node_volume_size,
294 symmetric_nat_gateway_vm_size,
295 symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
296 symmetric_private_node_volume_size,
297 tfvars_filename: environment_details
298 .environment_type
299 .get_tfvars_filename(name),
300 uploader_vm_count: Some(uploader_vm_count),
301 uploader_vm_size,
302 };
303
304 Ok(options)
305 }
306}
307
308impl TestnetDeployer {
309 pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
311 let start = Instant::now();
312 println!("Selecting {} workspace...", options.name);
313 self.terraform_runner.workspace_select(&options.name)?;
314
315 let args = build_terraform_args(options)?;
316
317 println!("Running terraform apply...");
318 self.terraform_runner
319 .apply(args, Some(options.tfvars_filename.clone()))?;
320 print_duration(start.elapsed());
321 Ok(())
322 }
323}
324
325pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
327 let mut args = Vec::new();
328
329 if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
330 args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
331 }
332
333 if let Some(genesis_vm_count) = options.genesis_vm_count {
334 args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
335 }
336
337 if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
338 args.push((
339 "peer_cache_node_vm_count".to_string(),
340 peer_cache_node_vm_count.to_string(),
341 ));
342 }
343 if let Some(node_vm_count) = options.node_vm_count {
344 args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
345 }
346
347 if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
348 args.push((
349 "symmetric_private_node_vm_count".to_string(),
350 symmetric_private_node_vm_count.to_string(),
351 ));
352 }
353 if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
354 args.push((
355 "full_cone_private_node_vm_count".to_string(),
356 full_cone_private_node_vm_count.to_string(),
357 ));
358 }
359
360 if let Some(evm_node_count) = options.evm_node_count {
361 args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
362 }
363
364 if let Some(uploader_vm_count) = options.uploader_vm_count {
365 args.push((
366 "uploader_vm_count".to_string(),
367 uploader_vm_count.to_string(),
368 ));
369 }
370
371 args.push((
372 "use_custom_bin".to_string(),
373 options.enable_build_vm.to_string(),
374 ));
375
376 if let Some(node_vm_size) = &options.node_vm_size {
377 args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
378 }
379
380 if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
381 args.push((
382 "peer_cache_droplet_size".to_string(),
383 peer_cache_vm_size.clone(),
384 ));
385 }
386
387 if let Some(uploader_vm_size) = &options.uploader_vm_size {
388 args.push((
389 "uploader_droplet_size".to_string(),
390 uploader_vm_size.clone(),
391 ));
392 }
393
394 if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
395 args.push((
396 "evm_node_droplet_size".to_string(),
397 evm_node_vm_size.clone(),
398 ));
399 }
400
401 if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
402 args.push((
403 "peer_cache_node_volume_size".to_string(),
404 peer_cache_node_volume_size.to_string(),
405 ));
406 }
407 if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
408 args.push((
409 "genesis_node_volume_size".to_string(),
410 genesis_node_volume_size.to_string(),
411 ));
412 }
413 if let Some(node_volume_size) = options.node_volume_size {
414 args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
415 }
416
417 if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
418 args.push((
419 "full_cone_nat_gateway_droplet_size".to_string(),
420 full_cone_gateway_vm_size.clone(),
421 ));
422 }
423 if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
424 args.push((
425 "full_cone_private_node_volume_size".to_string(),
426 full_cone_private_node_volume_size.to_string(),
427 ));
428 }
429
430 if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
431 args.push((
432 "symmetric_nat_gateway_droplet_size".to_string(),
433 nat_gateway_vm_size.clone(),
434 ));
435 }
436 if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
437 args.push((
438 "symmetric_private_node_volume_size".to_string(),
439 symmetric_private_node_volume_size.to_string(),
440 ));
441 }
442
443 Ok(args)
444}