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 debug!("Peer cache node volume size: {volume_size:?}");
94 let vm_size = get_value_for_resource(&resources, PEER_CACHE_NODE, SIZE)?;
95 debug!("Peer cache node size: {vm_size:?}");
96 let image_id = get_value_for_resource(&resources, PEER_CACHE_NODE, IMAGE)?;
97 debug!("Peer cache node image id: {image_id:?}");
98
99 (volume_size, vm_size, image_id)
100 } else {
101 (None, None, None)
102 };
103
104 let genesis_node_vm_count = resource_count(GENESIS_NODE);
105 debug!("Genesis node count: {genesis_node_vm_count}");
106 let genesis_node_volume_size = if genesis_node_vm_count > 0 {
107 get_value_for_resource(&resources, GENESIS_NODE_ATTACHED_VOLUME, SIZE)?
108 } else {
109 None
110 };
111 debug!("Genesis node volume size: {genesis_node_volume_size:?}");
112
113 let node_vm_count = resource_count(NODE);
114 debug!("Node count: {node_vm_count}");
115 let node_volume_size = if node_vm_count > 0 {
116 get_value_for_resource(&resources, NODE_ATTACHED_VOLUME, SIZE)?
117 } else {
118 None
119 };
120 debug!("Node volume size: {node_volume_size:?}");
121
122 let mut nat_gateway_image_id: Option<String> = None;
123 let symmetric_private_node_vm_count = resource_count(SYMMETRIC_PRIVATE_NODE);
124 debug!("Symmetric private node count: {symmetric_private_node_vm_count}");
125 let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
126 if symmetric_private_node_vm_count > 0 {
127 let symmetric_private_node_volume_size = get_value_for_resource(
128 &resources,
129 SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME,
130 SIZE,
131 )?;
132 debug!(
133 "Symmetric private node volume size: {symmetric_private_node_volume_size:?}"
134 );
135 let symmetric_nat_gateway_vm_size =
137 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, SIZE)?;
138
139 debug!("Symmetric nat gateway size: {symmetric_nat_gateway_vm_size:?}");
140
141 nat_gateway_image_id =
142 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, IMAGE)?;
143 debug!("Nat gateway image: {nat_gateway_image_id:?}");
144
145 (
146 symmetric_private_node_volume_size,
147 symmetric_nat_gateway_vm_size,
148 )
149 } else {
150 (None, None)
151 };
152
153 let full_cone_private_node_vm_count = resource_count(FULL_CONE_PRIVATE_NODE);
154 debug!("Full cone private node count: {full_cone_private_node_vm_count}");
155 let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
156 if full_cone_private_node_vm_count > 0 {
157 let full_cone_private_node_volume_size = get_value_for_resource(
158 &resources,
159 FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME,
160 SIZE,
161 )?;
162 debug!(
163 "Full cone private node volume size: {full_cone_private_node_volume_size:?}"
164 );
165 let full_cone_nat_gateway_vm_size =
167 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, SIZE)?;
168 debug!("Full cone nat gateway size: {full_cone_nat_gateway_vm_size:?}");
169
170 nat_gateway_image_id =
171 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, IMAGE)?;
172 debug!("Nat gateway image: {nat_gateway_image_id:?}");
173
174 (
175 full_cone_private_node_volume_size,
176 full_cone_nat_gateway_vm_size,
177 )
178 } else {
179 (None, None)
180 };
181
182 let uploader_vm_count = resource_count(UPLOADER);
183 debug!("Uploader count: {uploader_vm_count}");
184 let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
185 let vm_size = get_value_for_resource(&resources, UPLOADER, SIZE)?;
186 debug!("Uploader size: {vm_size:?}");
187 let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?;
188 debug!("Uploader image id: {image_id:?}");
189 (vm_size, image_id)
190 } else {
191 (None, None)
192 };
193
194 let build_vm_count = resource_count(BUILD_VM);
195 debug!("Build VM count: {build_vm_count}");
196 let enable_build_vm = build_vm_count > 0;
197
198 let (node_vm_size, node_image_id) = if node_vm_count > 0 {
200 let vm_size = get_value_for_resource(&resources, NODE, SIZE)?;
201 debug!("Node size obtained from {NODE}: {vm_size:?}");
202 let image_id = get_value_for_resource(&resources, NODE, IMAGE)?;
203 debug!("Node image id obtained from {NODE}: {image_id:?}");
204 (vm_size, image_id)
205 } else if symmetric_private_node_vm_count > 0 {
206 let vm_size = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, SIZE)?;
207 debug!("Node size obtained from {SYMMETRIC_PRIVATE_NODE}: {vm_size:?}");
208 let image_id = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, IMAGE)?;
209 debug!("Node image id obtained from {SYMMETRIC_PRIVATE_NODE}: {image_id:?}");
210 (vm_size, image_id)
211 } else if full_cone_private_node_vm_count > 0 {
212 let vm_size = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, SIZE)?;
213 debug!("Node size obtained from {FULL_CONE_PRIVATE_NODE}: {vm_size:?}");
214 let image_id = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, IMAGE)?;
215 debug!("Node image id obtained from {FULL_CONE_PRIVATE_NODE}: {image_id:?}");
216 (vm_size, image_id)
217 } else {
218 (None, None)
219 };
220
221 let evm_node_count = resource_count(EVM_NODE);
222 debug!("EVM node count: {evm_node_count}");
223 let (evm_node_vm_size, evm_node_image_id) = if evm_node_count > 0 {
224 let emv_node_vm_size = get_value_for_resource(&resources, EVM_NODE, SIZE)?;
225 debug!("EVM node size: {emv_node_vm_size:?}");
226 let evm_node_image_id = get_value_for_resource(&resources, EVM_NODE, IMAGE)?;
227 debug!("EVM node image id: {evm_node_image_id:?}");
228 (emv_node_vm_size, evm_node_image_id)
229 } else {
230 (None, None)
231 };
232
233 let options = Self {
234 enable_build_vm,
235 evm_node_count: Some(evm_node_count),
236 evm_node_vm_size,
237 evm_node_image_id,
238 full_cone_nat_gateway_vm_size,
239 full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
240 full_cone_private_node_volume_size,
241 genesis_vm_count: Some(genesis_node_vm_count),
242 genesis_node_volume_size,
243 name: name.to_string(),
244 nat_gateway_image_id,
245 node_image_id,
246 node_vm_count: Some(node_vm_count),
247 node_vm_size,
248 node_volume_size,
249 peer_cache_image_id,
250 peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
251 peer_cache_node_vm_size,
252 peer_cache_node_volume_size,
253 symmetric_nat_gateway_vm_size,
254 symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
255 symmetric_private_node_volume_size,
256 tfvars_filename: environment_details
257 .map(|details| details.environment_type.get_tfvars_filename(name)),
258 uploader_vm_count: Some(uploader_vm_count),
259 uploader_vm_size,
260 uploader_image_id,
261 };
262
263 Ok(options)
264 }
265}
266
267impl TestnetDeployer {
268 pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
270 let start = Instant::now();
271 println!("Selecting {} workspace...", options.name);
272 self.terraform_runner.workspace_select(&options.name)?;
273
274 let args = build_terraform_args(options)?;
275
276 println!("Running terraform apply...");
277 self.terraform_runner
278 .apply(args, options.tfvars_filename.clone())?;
279 print_duration(start.elapsed());
280 Ok(())
281 }
282}
283
284#[derive(Clone, Debug)]
285pub struct UploaderInfraRunOptions {
286 pub enable_build_vm: bool,
287 pub name: String,
288 pub tfvars_filename: String,
289 pub uploader_vm_count: Option<u16>,
290 pub uploader_vm_size: Option<String>,
291 pub uploader_image_id: Option<String>,
293}
294
295impl UploaderInfraRunOptions {
296 pub async fn generate_existing(
298 name: &str,
299 terraform_runner: &TerraformRunner,
300 environment_details: &EnvironmentDetails,
301 ) -> Result<Self> {
302 let resources = terraform_runner.show(name)?;
303
304 let resource_count = |resource_name: &str| -> u16 {
305 resources
306 .iter()
307 .filter(|r| r.resource_name == resource_name)
308 .count() as u16
309 };
310
311 let uploader_vm_count = resource_count(UPLOADER);
312 debug!("Uploader count: {uploader_vm_count}");
313 let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
314 let vm_size = get_value_for_resource(&resources, UPLOADER, SIZE)?;
315 debug!("Uploader size: {vm_size:?}");
316 let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?;
317 debug!("Uploader image id: {image_id:?}");
318 (vm_size, image_id)
319 } else {
320 (None, None)
321 };
322
323 let build_vm_count = resource_count(BUILD_VM);
324 debug!("Build VM count: {build_vm_count}");
325 let enable_build_vm = build_vm_count > 0;
326
327 let options = Self {
328 enable_build_vm,
329 name: name.to_string(),
330 tfvars_filename: environment_details
331 .environment_type
332 .get_tfvars_filename(name),
333 uploader_vm_count: Some(uploader_vm_count),
334 uploader_vm_size,
335 uploader_image_id,
336 };
337
338 Ok(options)
339 }
340
341 pub fn build_terraform_args(&self) -> Result<Vec<(String, String)>> {
342 let mut args = Vec::new();
343
344 args.push((
345 "use_custom_bin".to_string(),
346 self.enable_build_vm.to_string(),
347 ));
348
349 if let Some(uploader_vm_count) = self.uploader_vm_count {
350 args.push((
351 "uploader_vm_count".to_string(),
352 uploader_vm_count.to_string(),
353 ));
354 }
355 if let Some(uploader_vm_size) = &self.uploader_vm_size {
356 args.push((
357 "uploader_droplet_size".to_string(),
358 uploader_vm_size.clone(),
359 ));
360 }
361 if let Some(uploader_image_id) = &self.uploader_image_id {
362 args.push((
363 "uploader_droplet_image_id".to_string(),
364 uploader_image_id.clone(),
365 ));
366 }
367
368 Ok(args)
369 }
370}
371
372pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
374 let mut args = Vec::new();
375
376 args.push((
377 "use_custom_bin".to_string(),
378 options.enable_build_vm.to_string(),
379 ));
380
381 if let Some(evm_node_count) = options.evm_node_count {
382 args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
383 }
384
385 if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
386 args.push((
387 "evm_node_droplet_size".to_string(),
388 evm_node_vm_size.clone(),
389 ));
390 }
391
392 if let Some(emv_node_image_id) = &options.evm_node_image_id {
393 args.push((
394 "evm_node_droplet_image_id".to_string(),
395 emv_node_image_id.clone(),
396 ));
397 }
398
399 if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
400 args.push((
401 "full_cone_nat_gateway_droplet_size".to_string(),
402 full_cone_gateway_vm_size.clone(),
403 ));
404 }
405
406 if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
407 args.push((
408 "full_cone_private_node_vm_count".to_string(),
409 full_cone_private_node_vm_count.to_string(),
410 ));
411 }
412
413 if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
414 args.push((
415 "full_cone_private_node_volume_size".to_string(),
416 full_cone_private_node_volume_size.to_string(),
417 ));
418 }
419
420 if let Some(genesis_vm_count) = options.genesis_vm_count {
421 args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
422 }
423
424 if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
425 args.push((
426 "genesis_node_volume_size".to_string(),
427 genesis_node_volume_size.to_string(),
428 ));
429 }
430
431 if let Some(nat_gateway_image_id) = &options.nat_gateway_image_id {
432 args.push((
433 "nat_gateway_droplet_image_id".to_string(),
434 nat_gateway_image_id.clone(),
435 ));
436 }
437
438 if let Some(node_image_id) = &options.node_image_id {
439 args.push(("node_droplet_image_id".to_string(), node_image_id.clone()));
440 }
441
442 if let Some(node_vm_count) = options.node_vm_count {
443 args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
444 }
445
446 if let Some(node_vm_size) = &options.node_vm_size {
447 args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
448 }
449
450 if let Some(node_volume_size) = options.node_volume_size {
451 args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
452 }
453
454 if let Some(peer_cache_image_id) = &options.peer_cache_image_id {
455 args.push((
456 "peer_cache_droplet_image_id".to_string(),
457 peer_cache_image_id.clone(),
458 ));
459 }
460
461 if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
462 args.push((
463 "peer_cache_node_vm_count".to_string(),
464 peer_cache_node_vm_count.to_string(),
465 ));
466 }
467
468 if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
469 args.push((
470 "peer_cache_droplet_size".to_string(),
471 peer_cache_vm_size.clone(),
472 ));
473 }
474
475 if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
476 args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
477 }
478
479 if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
480 args.push((
481 "peer_cache_node_volume_size".to_string(),
482 peer_cache_node_volume_size.to_string(),
483 ));
484 }
485
486 if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
487 args.push((
488 "symmetric_nat_gateway_droplet_size".to_string(),
489 nat_gateway_vm_size.clone(),
490 ));
491 }
492
493 if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
494 args.push((
495 "symmetric_private_node_vm_count".to_string(),
496 symmetric_private_node_vm_count.to_string(),
497 ));
498 }
499
500 if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
501 args.push((
502 "symmetric_private_node_volume_size".to_string(),
503 symmetric_private_node_volume_size.to_string(),
504 ));
505 }
506
507 if let Some(uploader_image_id) = &options.uploader_image_id {
508 args.push((
509 "uploader_droplet_image_id".to_string(),
510 uploader_image_id.clone(),
511 ));
512 }
513
514 if let Some(uploader_vm_count) = options.uploader_vm_count {
515 args.push((
516 "uploader_vm_count".to_string(),
517 uploader_vm_count.to_string(),
518 ));
519 }
520
521 if let Some(uploader_vm_size) = &options.uploader_vm_size {
522 args.push((
523 "uploader_droplet_size".to_string(),
524 uploader_vm_size.clone(),
525 ));
526 }
527
528 Ok(args)
529}
530
531pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
534 terraform_runner.init()?;
535 let workspaces = terraform_runner.workspace_list()?;
536 if !workspaces.contains(&name.to_string()) {
537 return Err(Error::EnvironmentDoesNotExist(name.to_string()));
538 }
539 terraform_runner.workspace_select(name)?;
540 println!("Selected {name} workspace");
541 Ok(())
542}
543
544pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
545 terraform_runner.workspace_select("dev")?;
549 terraform_runner.workspace_delete(name)?;
550 println!("Deleted {name} workspace");
551 Ok(())
552}
553
554fn get_value_for_resource<T>(
556 resources: &[TerraformResource],
557 resource_name: &str,
558 field_name: &str,
559) -> Result<Option<T>, Error>
560where
561 T: From<TerraformValue>,
562{
563 let field_value = resources
564 .iter()
565 .filter(|r| r.resource_name == resource_name)
566 .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
567 if let Some(value) = r.values.get(field_name) {
568 match acc_value {
569 Some(ref existing_value) if existing_value != value => {
570 log::error!("Expected value: {existing_value}, got value: {value}");
571 Err(Error::TerraformResourceValueMismatch {
572 expected: existing_value.to_string(),
573 actual: value.to_string(),
574 })
575 }
576 _ => Ok(Some(value.clone())),
577 }
578 } else {
579 Ok(acc_value)
580 }
581 })?;
582
583 Ok(field_value.map(TerraformValue::from).map(T::from))
584}
585
586#[derive(Debug, Clone)]
588enum TerraformValue {
589 String(String),
590 Number(u64),
591 Bool(bool),
592 Other(serde_json::Value),
593}
594
595impl From<serde_json::Value> for TerraformValue {
596 fn from(value: serde_json::Value) -> Self {
597 if value.is_string() {
598 TerraformValue::String(value.as_str().unwrap().to_string())
601 } else if value.is_u64() {
602 TerraformValue::Number(value.as_u64().unwrap())
604 } else if value.is_boolean() {
605 TerraformValue::Bool(value.as_bool().unwrap())
607 } else {
608 TerraformValue::Other(value)
609 }
610 }
611}
612
613impl From<TerraformValue> for String {
615 fn from(value: TerraformValue) -> Self {
616 match value {
617 TerraformValue::String(s) => s,
618 TerraformValue::Number(n) => n.to_string(),
619 TerraformValue::Bool(b) => b.to_string(),
620 TerraformValue::Other(v) => v.to_string(),
621 }
622 }
623}
624
625impl From<TerraformValue> for u16 {
626 fn from(value: TerraformValue) -> Self {
627 match value {
628 TerraformValue::Number(n) => n as u16,
629 TerraformValue::String(s) => s.parse().unwrap_or(0),
630 _ => 0,
631 }
632 }
633}