1use log::debug;
8
9use crate::{
10 error::{Error, Result},
11 print_duration,
12 terraform::{TerraformResource, TerraformRunner},
13 EnvironmentDetails, TestnetDeployer,
14};
15use std::time::Instant;
16
17const BUILD_VM: &str = "build";
18const EVM_NODE: &str = "evm_node";
19const FULL_CONE_NAT_GATEWAY: &str = "full_cone_nat_gateway";
20const FULL_CONE_PRIVATE_NODE: &str = "full_cone_private_node";
21const FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME: &str = "full_cone_private_node_attached_volume";
22const GENESIS_NODE: &str = "genesis_bootstrap";
23const GENESIS_NODE_ATTACHED_VOLUME: &str = "genesis_node_attached_volume";
24const NODE: &str = "node";
25const NODE_ATTACHED_VOLUME: &str = "node_attached_volume";
26const PEER_CACHE_NODE: &str = "peer_cache_node";
27const PEER_CACHE_NODE_ATTACHED_VOLUME: &str = "peer_cache_node_attached_volume";
28const SYMMETRIC_NAT_GATEWAY: &str = "symmetric_nat_gateway";
29const SYMMETRIC_PRIVATE_NODE: &str = "symmetric_private_node";
30const SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME: &str = "symmetric_private_node_attached_volume";
31const UPLOADER: &str = "uploader";
32
33const SIZE: &str = "size";
34const IMAGE: &str = "image";
35
36#[derive(Clone, Debug)]
37pub struct InfraRunOptions {
38 pub enable_build_vm: bool,
39 pub evm_node_count: Option<u16>,
40 pub evm_node_vm_size: Option<String>,
41 pub evm_node_image_id: Option<String>,
43 pub full_cone_nat_gateway_vm_size: Option<String>,
44 pub full_cone_private_node_vm_count: Option<u16>,
45 pub full_cone_private_node_volume_size: Option<u16>,
46 pub genesis_vm_count: Option<u16>,
47 pub genesis_node_volume_size: Option<u16>,
48 pub name: String,
49 pub nat_gateway_image_id: Option<String>,
51 pub node_image_id: Option<String>,
53 pub node_vm_count: Option<u16>,
54 pub node_vm_size: Option<String>,
55 pub node_volume_size: Option<u16>,
56 pub peer_cache_image_id: Option<String>,
58 pub peer_cache_node_vm_count: Option<u16>,
59 pub peer_cache_node_vm_size: Option<String>,
60 pub peer_cache_node_volume_size: Option<u16>,
61 pub symmetric_nat_gateway_vm_size: Option<String>,
62 pub symmetric_private_node_vm_count: Option<u16>,
63 pub symmetric_private_node_volume_size: Option<u16>,
64 pub tfvars_filename: Option<String>,
65 pub uploader_image_id: Option<String>,
67 pub uploader_vm_count: Option<u16>,
68 pub uploader_vm_size: Option<String>,
69}
70
71impl InfraRunOptions {
72 pub async fn generate_existing(
74 name: &str,
75 terraform_runner: &TerraformRunner,
76 environment_details: Option<&EnvironmentDetails>,
77 ) -> Result<Self> {
78 let resources = terraform_runner.show(name)?;
79
80 let resource_count = |resource_name: &str| -> u16 {
81 resources
82 .iter()
83 .filter(|r| r.resource_name == resource_name)
84 .count() as u16
85 };
86
87 let peer_cache_node_vm_count = resource_count(PEER_CACHE_NODE);
88 debug!("Peer cache node count: {peer_cache_node_vm_count}");
89 let (peer_cache_node_volume_size, peer_cache_node_vm_size, peer_cache_image_id) =
90 if peer_cache_node_vm_count > 0 {
91 let volume_size =
92 get_value_for_resource(&resources, PEER_CACHE_NODE_ATTACHED_VOLUME, SIZE)?
93 .and_then(|size| size.as_u64())
94 .map(|size| size as u16);
95 debug!("Peer cache node volume size: {volume_size:?}");
96 let vm_size = get_value_for_resource(&resources, PEER_CACHE_NODE, SIZE)?
97 .map(|size| size.to_string());
98 debug!("Peer cache node size: {vm_size:?}");
99 let image_id = get_value_for_resource(&resources, PEER_CACHE_NODE, IMAGE)?
100 .map(|image_id| image_id.to_string());
101 debug!("Peer cache node image id: {image_id:?}");
102
103 (volume_size, vm_size, image_id)
104 } else {
105 (None, None, None)
106 };
107
108 let genesis_node_vm_count = resource_count(GENESIS_NODE);
109 debug!("Genesis node count: {genesis_node_vm_count}");
110 let genesis_node_volume_size = if genesis_node_vm_count > 0 {
111 get_value_for_resource(&resources, GENESIS_NODE_ATTACHED_VOLUME, SIZE)?
112 .and_then(|size| size.as_u64())
113 .map(|size| size as u16)
114 } else {
115 None
116 };
117 debug!("Genesis node volume size: {genesis_node_volume_size:?}");
118
119 let node_vm_count = resource_count(NODE);
120 debug!("Node count: {node_vm_count}");
121 let node_volume_size = if node_vm_count > 0 {
122 get_value_for_resource(&resources, NODE_ATTACHED_VOLUME, SIZE)?
123 .and_then(|size| size.as_u64())
124 .map(|size| size as u16)
125 } else {
126 None
127 };
128 debug!("Node volume size: {node_volume_size:?}");
129
130 let mut nat_gateway_image_id = None;
131 let symmetric_private_node_vm_count = resource_count(SYMMETRIC_PRIVATE_NODE);
132 debug!("Symmetric private node count: {symmetric_private_node_vm_count}");
133 let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
134 if symmetric_private_node_vm_count > 0 {
135 let symmetric_private_node_volume_size = get_value_for_resource(
136 &resources,
137 SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME,
138 SIZE,
139 )?
140 .and_then(|size| size.as_u64())
141 .map(|size| size as u16);
142 debug!(
143 "Symmetric private node volume size: {symmetric_private_node_volume_size:?}"
144 );
145 let symmetric_nat_gateway_vm_size =
147 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, SIZE)?
148 .map(|size| size.to_string());
149
150 debug!("Symmetric nat gateway size: {symmetric_nat_gateway_vm_size:?}");
151
152 if let Some(nat_gateway_image) =
153 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, IMAGE)?
154 {
155 debug!("Nat gateway image: {nat_gateway_image}");
156 nat_gateway_image_id = Some(nat_gateway_image.to_string());
157 }
158
159 (
160 symmetric_private_node_volume_size,
161 symmetric_nat_gateway_vm_size,
162 )
163 } else {
164 (None, None)
165 };
166
167 let full_cone_private_node_vm_count = resource_count(FULL_CONE_PRIVATE_NODE);
168 debug!("Full cone private node count: {full_cone_private_node_vm_count}");
169 let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
170 if full_cone_private_node_vm_count > 0 {
171 let full_cone_private_node_volume_size = get_value_for_resource(
172 &resources,
173 FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME,
174 SIZE,
175 )?
176 .and_then(|size| size.as_u64())
177 .map(|size| size as u16);
178 debug!(
179 "Full cone private node volume size: {full_cone_private_node_volume_size:?}"
180 );
181 let full_cone_nat_gateway_vm_size =
183 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, SIZE)?
184 .map(|size| size.to_string());
185 debug!("Full cone nat gateway size: {full_cone_nat_gateway_vm_size:?}");
186
187 if let Some(nat_gateway_image) =
188 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, IMAGE)?
189 {
190 debug!("Nat gateway image: {nat_gateway_image}");
191 nat_gateway_image_id = Some(nat_gateway_image.to_string());
192 }
193
194 (
195 full_cone_private_node_volume_size,
196 full_cone_nat_gateway_vm_size,
197 )
198 } else {
199 (None, None)
200 };
201
202 let uploader_vm_count = resource_count(UPLOADER);
203 debug!("Uploader count: {uploader_vm_count}");
204 let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
205 let vm_size =
206 get_value_for_resource(&resources, UPLOADER, SIZE)?.map(|size| size.to_string());
207 debug!("Uploader size: {vm_size:?}");
208 let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?
209 .map(|image_id| image_id.to_string());
210 debug!("Uploader image id: {image_id:?}");
211 (vm_size, image_id)
212 } else {
213 (None, None)
214 };
215
216 let build_vm_count = resource_count(BUILD_VM);
217 debug!("Build VM count: {build_vm_count}");
218 let enable_build_vm = build_vm_count > 0;
219
220 let (node_vm_size, node_image_id) = if node_vm_count > 0 {
222 let vm_size =
223 get_value_for_resource(&resources, NODE, SIZE)?.map(|size| size.to_string());
224 debug!("Node size obtained from {NODE}: {vm_size:?}");
225 let image_id = get_value_for_resource(&resources, NODE, IMAGE)?
226 .map(|image_id| image_id.to_string());
227 debug!("Node image id obtained from {NODE}: {image_id:?}");
228 (vm_size, image_id)
229 } else if symmetric_private_node_vm_count > 0 {
230 let vm_size = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, SIZE)?
231 .map(|size| size.to_string());
232 debug!("Node size obtained from {SYMMETRIC_PRIVATE_NODE}: {vm_size:?}");
233 let image_id = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, IMAGE)?
234 .map(|image_id| image_id.to_string());
235 debug!("Node image id obtained from {SYMMETRIC_PRIVATE_NODE}: {image_id:?}");
236 (vm_size, image_id)
237 } else if full_cone_private_node_vm_count > 0 {
238 let vm_size = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, SIZE)?
239 .map(|size| size.to_string());
240 debug!("Node size obtained from {FULL_CONE_PRIVATE_NODE}: {vm_size:?}");
241 let image_id = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, IMAGE)?
242 .map(|image_id| image_id.to_string());
243 debug!("Node image id obtained from {FULL_CONE_PRIVATE_NODE}: {image_id:?}");
244 (vm_size, image_id)
245 } else {
246 (None, None)
247 };
248
249 let evm_node_count = resource_count(EVM_NODE);
250 debug!("EVM node count: {evm_node_count}");
251 let (evm_node_vm_size, evm_node_image_id) = if evm_node_count > 0 {
252 let emv_node_vm_size =
253 get_value_for_resource(&resources, EVM_NODE, SIZE)?.map(|size| size.to_string());
254 debug!("EVM node size: {emv_node_vm_size:?}");
255 let evm_node_image_id = get_value_for_resource(&resources, EVM_NODE, IMAGE)?
256 .map(|image_id| image_id.to_string());
257 debug!("EVM node image id: {evm_node_image_id:?}");
258 (emv_node_vm_size, evm_node_image_id)
259 } else {
260 (None, None)
261 };
262
263 let options = Self {
264 enable_build_vm,
265 evm_node_count: Some(evm_node_count),
266 evm_node_vm_size,
267 evm_node_image_id,
268 full_cone_nat_gateway_vm_size,
269 full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
270 full_cone_private_node_volume_size,
271 genesis_vm_count: Some(genesis_node_vm_count),
272 genesis_node_volume_size,
273 name: name.to_string(),
274 nat_gateway_image_id,
275 node_image_id,
276 node_vm_count: Some(node_vm_count),
277 node_vm_size,
278 node_volume_size,
279 peer_cache_image_id,
280 peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
281 peer_cache_node_vm_size,
282 peer_cache_node_volume_size,
283 symmetric_nat_gateway_vm_size,
284 symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
285 symmetric_private_node_volume_size,
286 tfvars_filename: environment_details
287 .map(|details| details.environment_type.get_tfvars_filename(name)),
288 uploader_vm_count: Some(uploader_vm_count),
289 uploader_vm_size,
290 uploader_image_id,
291 };
292
293 Ok(options)
294 }
295}
296
297impl TestnetDeployer {
298 pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
300 let start = Instant::now();
301 println!("Selecting {} workspace...", options.name);
302 self.terraform_runner.workspace_select(&options.name)?;
303
304 let args = build_terraform_args(options)?;
305
306 println!("Running terraform apply...");
307 self.terraform_runner
308 .apply(args, options.tfvars_filename.clone())?;
309 print_duration(start.elapsed());
310 Ok(())
311 }
312}
313
314#[derive(Clone, Debug)]
315pub struct UploaderInfraRunOptions {
316 pub enable_build_vm: bool,
317 pub name: String,
318 pub tfvars_filename: String,
319 pub uploader_vm_count: Option<u16>,
320 pub uploader_vm_size: Option<String>,
321 pub uploader_image_id: Option<String>,
323}
324
325impl UploaderInfraRunOptions {
326 pub async fn generate_existing(
328 name: &str,
329 terraform_runner: &TerraformRunner,
330 environment_details: &EnvironmentDetails,
331 ) -> Result<Self> {
332 let resources = terraform_runner.show(name)?;
333
334 let resource_count = |resource_name: &str| -> u16 {
335 resources
336 .iter()
337 .filter(|r| r.resource_name == resource_name)
338 .count() as u16
339 };
340
341 let uploader_vm_count = resource_count(UPLOADER);
342 debug!("Uploader count: {uploader_vm_count}");
343 let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
344 let vm_size =
345 get_value_for_resource(&resources, UPLOADER, SIZE)?.map(|size| size.to_string());
346 debug!("Uploader size: {vm_size:?}");
347 let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?
348 .map(|image_id| image_id.to_string());
349 debug!("Uploader image id: {image_id:?}");
350 (vm_size, image_id)
351 } else {
352 (None, None)
353 };
354
355 let build_vm_count = resource_count(BUILD_VM);
356 debug!("Build VM count: {build_vm_count}");
357 let enable_build_vm = build_vm_count > 0;
358
359 let options = Self {
360 enable_build_vm,
361 name: name.to_string(),
362 tfvars_filename: environment_details
363 .environment_type
364 .get_tfvars_filename(name),
365 uploader_vm_count: Some(uploader_vm_count),
366 uploader_vm_size,
367 uploader_image_id,
368 };
369
370 Ok(options)
371 }
372
373 pub fn build_terraform_args(&self) -> Result<Vec<(String, String)>> {
374 let mut args = Vec::new();
375
376 args.push((
377 "use_custom_bin".to_string(),
378 self.enable_build_vm.to_string(),
379 ));
380
381 if let Some(uploader_vm_count) = self.uploader_vm_count {
382 args.push((
383 "uploader_vm_count".to_string(),
384 uploader_vm_count.to_string(),
385 ));
386 }
387 if let Some(uploader_vm_size) = &self.uploader_vm_size {
388 args.push((
389 "uploader_droplet_size".to_string(),
390 uploader_vm_size.clone(),
391 ));
392 }
393 if let Some(uploader_image_id) = &self.uploader_image_id {
394 args.push((
395 "uploader_droplet_image_id".to_string(),
396 uploader_image_id.clone(),
397 ));
398 }
399
400 Ok(args)
401 }
402}
403
404fn get_value_for_resource(
406 resources: &[TerraformResource],
407 resource_name: &str,
408 field_name: &str,
409) -> Result<Option<serde_json::Value>, Error> {
410 let field_value = resources
411 .iter()
412 .filter(|r| r.resource_name == resource_name)
413 .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
414 if let Some(value) = r.values.get(field_name) {
415 match acc_value {
416 Some(ref existing_value) if existing_value != value => {
417 log::error!("Expected value: {existing_value}, got value: {value}");
418 Err(Error::TerraformResourceValueMismatch {
419 expected: existing_value.to_string(),
420 actual: value.to_string(),
421 })
422 }
423 _ => Ok(Some(value.clone())),
424 }
425 } else {
426 Ok(acc_value)
427 }
428 })?;
429
430 Ok(field_value)
431}
432
433pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
435 let mut args = Vec::new();
436
437 args.push((
438 "use_custom_bin".to_string(),
439 options.enable_build_vm.to_string(),
440 ));
441
442 if let Some(evm_node_count) = options.evm_node_count {
443 args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
444 }
445
446 if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
447 args.push((
448 "evm_node_droplet_size".to_string(),
449 evm_node_vm_size.clone(),
450 ));
451 }
452
453 if let Some(emv_node_image_id) = &options.evm_node_image_id {
454 args.push((
455 "evm_node_droplet_image_id".to_string(),
456 emv_node_image_id.clone(),
457 ));
458 }
459
460 if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
461 args.push((
462 "full_cone_nat_gateway_droplet_size".to_string(),
463 full_cone_gateway_vm_size.clone(),
464 ));
465 }
466
467 if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
468 args.push((
469 "full_cone_private_node_vm_count".to_string(),
470 full_cone_private_node_vm_count.to_string(),
471 ));
472 }
473
474 if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
475 args.push((
476 "full_cone_private_node_volume_size".to_string(),
477 full_cone_private_node_volume_size.to_string(),
478 ));
479 }
480
481 if let Some(genesis_vm_count) = options.genesis_vm_count {
482 args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
483 }
484
485 if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
486 args.push((
487 "genesis_node_volume_size".to_string(),
488 genesis_node_volume_size.to_string(),
489 ));
490 }
491
492 if let Some(nat_gateway_image_id) = &options.nat_gateway_image_id {
493 args.push((
494 "nat_gateway_droplet_image_id".to_string(),
495 nat_gateway_image_id.clone(),
496 ));
497 }
498
499 if let Some(node_image_id) = &options.node_image_id {
500 args.push(("node_droplet_image_id".to_string(), node_image_id.clone()));
501 }
502
503 if let Some(node_vm_count) = options.node_vm_count {
504 args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
505 }
506
507 if let Some(node_vm_size) = &options.node_vm_size {
508 args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
509 }
510
511 if let Some(node_volume_size) = options.node_volume_size {
512 args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
513 }
514
515 if let Some(peer_cache_image_id) = &options.peer_cache_image_id {
516 args.push((
517 "peer_cache_droplet_image_id".to_string(),
518 peer_cache_image_id.clone(),
519 ));
520 }
521
522 if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
523 args.push((
524 "peer_cache_node_vm_count".to_string(),
525 peer_cache_node_vm_count.to_string(),
526 ));
527 }
528
529 if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
530 args.push((
531 "peer_cache_droplet_size".to_string(),
532 peer_cache_vm_size.clone(),
533 ));
534 }
535
536 if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
537 args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
538 }
539
540 if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
541 args.push((
542 "peer_cache_node_volume_size".to_string(),
543 peer_cache_node_volume_size.to_string(),
544 ));
545 }
546
547 if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
548 args.push((
549 "symmetric_nat_gateway_droplet_size".to_string(),
550 nat_gateway_vm_size.clone(),
551 ));
552 }
553
554 if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
555 args.push((
556 "symmetric_private_node_vm_count".to_string(),
557 symmetric_private_node_vm_count.to_string(),
558 ));
559 }
560
561 if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
562 args.push((
563 "symmetric_private_node_volume_size".to_string(),
564 symmetric_private_node_volume_size.to_string(),
565 ));
566 }
567
568 if let Some(uploader_image_id) = &options.uploader_image_id {
569 args.push((
570 "uploader_droplet_image_id".to_string(),
571 uploader_image_id.clone(),
572 ));
573 }
574
575 if let Some(uploader_vm_count) = options.uploader_vm_count {
576 args.push((
577 "uploader_vm_count".to_string(),
578 uploader_vm_count.to_string(),
579 ));
580 }
581
582 if let Some(uploader_vm_size) = &options.uploader_vm_size {
583 args.push((
584 "uploader_droplet_size".to_string(),
585 uploader_vm_size.clone(),
586 ));
587 }
588
589 Ok(args)
590}
591
592pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
595 terraform_runner.init()?;
596 let workspaces = terraform_runner.workspace_list()?;
597 if !workspaces.contains(&name.to_string()) {
598 return Err(Error::EnvironmentDoesNotExist(name.to_string()));
599 }
600 terraform_runner.workspace_select(name)?;
601 println!("Selected {name} workspace");
602 Ok(())
603}
604
605pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
606 terraform_runner.workspace_select("dev")?;
610 terraform_runner.workspace_delete(name)?;
611 println!("Deleted {name} workspace");
612 Ok(())
613}