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 CLIENT: &str = "ant_client";
19const EVM_NODE: &str = "evm_node";
20const FULL_CONE_NAT_GATEWAY: &str = "full_cone_nat_gateway";
21const FULL_CONE_PRIVATE_NODE: &str = "full_cone_private_node";
22const FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME: &str = "full_cone_private_node_attached_volume";
23const GENESIS_NODE: &str = "genesis_bootstrap";
24const GENESIS_NODE_ATTACHED_VOLUME: &str = "genesis_node_attached_volume";
25const NODE: &str = "node";
26const NODE_ATTACHED_VOLUME: &str = "node_attached_volume";
27const PEER_CACHE_NODE: &str = "peer_cache_node";
28const PEER_CACHE_NODE_ATTACHED_VOLUME: &str = "peer_cache_node_attached_volume";
29const SYMMETRIC_NAT_GATEWAY: &str = "symmetric_nat_gateway";
30const SYMMETRIC_PRIVATE_NODE: &str = "symmetric_private_node";
31const SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME: &str = "symmetric_private_node_attached_volume";
32
33const SIZE: &str = "size";
34const IMAGE: &str = "image";
35
36#[derive(Clone, Debug)]
37pub struct InfraRunOptions {
38 pub client_image_id: Option<String>,
40 pub client_vm_count: Option<u16>,
41 pub client_vm_size: Option<String>,
42 pub enable_build_vm: bool,
43 pub evm_node_count: Option<u16>,
44 pub evm_node_vm_size: Option<String>,
45 pub evm_node_image_id: Option<String>,
47 pub full_cone_vm_size: Option<String>,
48 pub full_cone_private_node_vm_count: Option<u16>,
49 pub full_cone_private_node_volume_size: Option<u16>,
50 pub genesis_vm_count: Option<u16>,
51 pub genesis_node_volume_size: Option<u16>,
52 pub name: String,
53 pub nat_gateway_image_id: Option<String>,
55 pub node_image_id: Option<String>,
57 pub node_vm_count: Option<u16>,
58 pub node_vm_size: Option<String>,
59 pub node_volume_size: Option<u16>,
60 pub peer_cache_image_id: Option<String>,
62 pub peer_cache_node_vm_count: Option<u16>,
63 pub peer_cache_node_vm_size: Option<String>,
64 pub peer_cache_node_volume_size: Option<u16>,
65 pub region: String,
66 pub symmetric_nat_gateway_vm_size: Option<String>,
67 pub symmetric_private_node_vm_count: Option<u16>,
68 pub symmetric_private_node_volume_size: Option<u16>,
69 pub tfvars_filenames: Option<Vec<String>>,
70}
71
72impl InfraRunOptions {
73 pub async fn generate_existing(
75 name: &str,
76 region: &str,
77 terraform_runner: &TerraformRunner,
78 environment_details: Option<&EnvironmentDetails>,
79 ) -> Result<Self> {
80 let resources = terraform_runner.show(name)?;
81
82 let resource_count = |resource_name: &str| -> u16 {
83 resources
84 .iter()
85 .filter(|r| r.resource_name == resource_name)
86 .count() as u16
87 };
88
89 let peer_cache_node_vm_count = resource_count(PEER_CACHE_NODE);
90 debug!("Peer cache node count: {peer_cache_node_vm_count}");
91 let (peer_cache_node_volume_size, peer_cache_node_vm_size, peer_cache_image_id) =
92 if peer_cache_node_vm_count > 0 {
93 let volume_size =
94 get_value_for_resource(&resources, PEER_CACHE_NODE_ATTACHED_VOLUME, SIZE)?;
95 debug!("Peer cache node volume size: {volume_size:?}");
96 let vm_size = get_value_for_resource(&resources, PEER_CACHE_NODE, SIZE)?;
97 debug!("Peer cache node size: {vm_size:?}");
98 let image_id = get_value_for_resource(&resources, PEER_CACHE_NODE, IMAGE)?;
99 debug!("Peer cache node image id: {image_id:?}");
100
101 (volume_size, vm_size, image_id)
102 } else {
103 (None, None, None)
104 };
105
106 let genesis_node_vm_count = resource_count(GENESIS_NODE);
107 debug!("Genesis node count: {genesis_node_vm_count}");
108 let genesis_node_volume_size = if genesis_node_vm_count > 0 {
109 get_value_for_resource(&resources, GENESIS_NODE_ATTACHED_VOLUME, SIZE)?
110 } else {
111 None
112 };
113 debug!("Genesis node volume size: {genesis_node_volume_size:?}");
114
115 let node_vm_count = resource_count(NODE);
116 debug!("Node count: {node_vm_count}");
117 let node_volume_size = if node_vm_count > 0 {
118 get_value_for_resource(&resources, NODE_ATTACHED_VOLUME, SIZE)?
119 } else {
120 None
121 };
122 debug!("Node volume size: {node_volume_size:?}");
123
124 let mut nat_gateway_image_id: Option<String> = None;
125 let symmetric_private_node_vm_count = resource_count(SYMMETRIC_PRIVATE_NODE);
126 debug!("Symmetric private node count: {symmetric_private_node_vm_count}");
127 let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
128 if symmetric_private_node_vm_count > 0 {
129 let symmetric_private_node_volume_size = get_value_for_resource(
130 &resources,
131 SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME,
132 SIZE,
133 )?;
134 debug!(
135 "Symmetric private node volume size: {symmetric_private_node_volume_size:?}"
136 );
137 let symmetric_nat_gateway_vm_size =
139 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, SIZE)?;
140
141 debug!("Symmetric nat gateway size: {symmetric_nat_gateway_vm_size:?}");
142
143 nat_gateway_image_id =
144 get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, IMAGE)?;
145 debug!("Nat gateway image: {nat_gateway_image_id:?}");
146
147 (
148 symmetric_private_node_volume_size,
149 symmetric_nat_gateway_vm_size,
150 )
151 } else {
152 (None, None)
153 };
154
155 let full_cone_private_node_vm_count = resource_count(FULL_CONE_PRIVATE_NODE);
156 debug!("Full cone private node count: {full_cone_private_node_vm_count}");
157 let (full_cone_private_node_volume_size, full_cone_vm_size) =
158 if full_cone_private_node_vm_count > 0 {
159 let full_cone_private_node_volume_size = get_value_for_resource(
160 &resources,
161 FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME,
162 SIZE,
163 )?;
164 debug!(
165 "Full cone private node volume size: {full_cone_private_node_volume_size:?}"
166 );
167 let full_cone_vm_size =
169 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, SIZE)?;
170 debug!("Full cone nat gateway size: {full_cone_vm_size:?}");
171
172 nat_gateway_image_id =
173 get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, IMAGE)?;
174 debug!("Nat gateway image: {nat_gateway_image_id:?}");
175
176 (full_cone_private_node_volume_size, full_cone_vm_size)
177 } else {
178 (None, None)
179 };
180
181 let client_vm_count = resource_count(CLIENT);
182 debug!("Client count: {client_vm_count}");
183 let (client_vm_size, client_image_id) = if client_vm_count > 0 {
184 let vm_size = get_value_for_resource(&resources, CLIENT, SIZE)?;
185 debug!("Client size: {vm_size:?}");
186 let image_id = get_value_for_resource(&resources, CLIENT, IMAGE)?;
187 debug!("Client image id: {image_id:?}");
188 (vm_size, image_id)
189 } else {
190 (None, None)
191 };
192
193 let build_vm_count = resource_count(BUILD_VM);
194 debug!("Build VM count: {build_vm_count}");
195 let enable_build_vm = build_vm_count > 0;
196
197 let (node_vm_size, node_image_id) = if node_vm_count > 0 {
199 let vm_size = get_value_for_resource(&resources, NODE, SIZE)?;
200 debug!("Node size obtained from {NODE}: {vm_size:?}");
201 let image_id = get_value_for_resource(&resources, NODE, IMAGE)?;
202 debug!("Node image id obtained from {NODE}: {image_id:?}");
203 (vm_size, image_id)
204 } else if symmetric_private_node_vm_count > 0 {
205 let vm_size = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, SIZE)?;
206 debug!("Node size obtained from {SYMMETRIC_PRIVATE_NODE}: {vm_size:?}");
207 let image_id = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, IMAGE)?;
208 debug!("Node image id obtained from {SYMMETRIC_PRIVATE_NODE}: {image_id:?}");
209 (vm_size, image_id)
210 } else if full_cone_private_node_vm_count > 0 {
211 let vm_size = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, SIZE)?;
212 debug!("Node size obtained from {FULL_CONE_PRIVATE_NODE}: {vm_size:?}");
213 let image_id = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, IMAGE)?;
214 debug!("Node image id obtained from {FULL_CONE_PRIVATE_NODE}: {image_id:?}");
215 (vm_size, image_id)
216 } else {
217 (None, None)
218 };
219
220 let evm_node_count = resource_count(EVM_NODE);
221 debug!("EVM node count: {evm_node_count}");
222 let (evm_node_vm_size, evm_node_image_id) = if evm_node_count > 0 {
223 let emv_node_vm_size = get_value_for_resource(&resources, EVM_NODE, SIZE)?;
224 debug!("EVM node size: {emv_node_vm_size:?}");
225 let evm_node_image_id = get_value_for_resource(&resources, EVM_NODE, IMAGE)?;
226 debug!("EVM node image id: {evm_node_image_id:?}");
227 (emv_node_vm_size, evm_node_image_id)
228 } else {
229 (None, None)
230 };
231
232 let options = Self {
233 client_image_id,
234 client_vm_count: Some(client_vm_count),
235 client_vm_size,
236 enable_build_vm,
237 evm_node_count: Some(evm_node_count),
238 evm_node_vm_size,
239 evm_node_image_id,
240 full_cone_vm_size,
241 full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
242 full_cone_private_node_volume_size,
243 genesis_vm_count: Some(genesis_node_vm_count),
244 genesis_node_volume_size,
245 name: name.to_string(),
246 nat_gateway_image_id,
247 node_image_id,
248 node_vm_count: Some(node_vm_count),
249 node_vm_size,
250 node_volume_size,
251 peer_cache_image_id,
252 peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
253 peer_cache_node_vm_size,
254 peer_cache_node_volume_size,
255 region: region.to_string(),
256 symmetric_nat_gateway_vm_size,
257 symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
258 symmetric_private_node_volume_size,
259 tfvars_filenames: environment_details
260 .map(|details| details.environment_type.get_tfvars_filenames(name, region)),
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_filenames.clone())?;
279 print_duration(start.elapsed());
280 Ok(())
281 }
282}
283
284#[derive(Clone, Debug)]
285pub struct ClientsInfraRunOptions {
286 pub client_image_id: Option<String>,
287 pub client_vm_count: Option<u16>,
288 pub client_vm_size: Option<String>,
289 pub enable_build_vm: bool,
291 pub name: String,
292 pub tfvars_filenames: Vec<String>,
293}
294
295impl ClientsInfraRunOptions {
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 client_vm_count = resource_count(CLIENT);
312 debug!("Client count: {client_vm_count}");
313 let (client_vm_size, client_image_id) = if client_vm_count > 0 {
314 let vm_size = get_value_for_resource(&resources, CLIENT, SIZE)?;
315 debug!("Client size: {vm_size:?}");
316 let image_id = get_value_for_resource(&resources, CLIENT, IMAGE)?;
317 debug!("Client 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 client_image_id,
329 client_vm_count: Some(client_vm_count),
330 client_vm_size,
331 enable_build_vm,
332 name: name.to_string(),
333 tfvars_filenames: environment_details
334 .environment_type
335 .get_tfvars_filenames(name, &environment_details.region),
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 if let Some(client_vm_count) = self.client_vm_count {
345 args.push((
346 "ant_client_vm_count".to_string(),
347 client_vm_count.to_string(),
348 ));
349 }
350 if let Some(client_vm_size) = &self.client_vm_size {
351 args.push((
352 "ant_client_droplet_size".to_string(),
353 client_vm_size.clone(),
354 ));
355 }
356 if let Some(client_image_id) = &self.client_image_id {
357 args.push((
358 "ant_client_droplet_image_id".to_string(),
359 client_image_id.clone(),
360 ));
361 }
362
363 args.push((
364 "use_custom_bin".to_string(),
365 self.enable_build_vm.to_string(),
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(("region".to_string(), options.region.clone()));
377
378 if let Some(client_image_id) = &options.client_image_id {
379 args.push((
380 "ant_client_droplet_image_id".to_string(),
381 client_image_id.clone(),
382 ));
383 }
384
385 if let Some(client_vm_count) = options.client_vm_count {
386 args.push((
387 "ant_client_vm_count".to_string(),
388 client_vm_count.to_string(),
389 ));
390 }
391
392 if let Some(client_vm_size) = &options.client_vm_size {
393 args.push((
394 "ant_client_droplet_size".to_string(),
395 client_vm_size.clone(),
396 ));
397 }
398
399 args.push((
400 "use_custom_bin".to_string(),
401 options.enable_build_vm.to_string(),
402 ));
403
404 if let Some(evm_node_count) = options.evm_node_count {
405 args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
406 }
407
408 if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
409 args.push((
410 "evm_node_droplet_size".to_string(),
411 evm_node_vm_size.clone(),
412 ));
413 }
414
415 if let Some(emv_node_image_id) = &options.evm_node_image_id {
416 args.push((
417 "evm_node_droplet_image_id".to_string(),
418 emv_node_image_id.clone(),
419 ));
420 }
421
422 if let Some(full_cone_vm_size) = &options.full_cone_vm_size {
423 args.push((
424 "full_cone_droplet_size".to_string(),
425 full_cone_vm_size.clone(),
426 ));
427 }
428
429 if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
430 args.push((
431 "full_cone_private_node_vm_count".to_string(),
432 full_cone_private_node_vm_count.to_string(),
433 ));
434 }
435
436 if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
437 args.push((
438 "full_cone_private_node_volume_size".to_string(),
439 full_cone_private_node_volume_size.to_string(),
440 ));
441 }
442
443 if let Some(genesis_vm_count) = options.genesis_vm_count {
444 args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
445 }
446
447 if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
448 args.push((
449 "genesis_node_volume_size".to_string(),
450 genesis_node_volume_size.to_string(),
451 ));
452 }
453
454 if let Some(nat_gateway_image_id) = &options.nat_gateway_image_id {
455 args.push((
456 "nat_gateway_droplet_image_id".to_string(),
457 nat_gateway_image_id.clone(),
458 ));
459 }
460
461 if let Some(node_image_id) = &options.node_image_id {
462 args.push(("node_droplet_image_id".to_string(), node_image_id.clone()));
463 }
464
465 if let Some(node_vm_count) = options.node_vm_count {
466 args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
467 }
468
469 if let Some(node_vm_size) = &options.node_vm_size {
470 args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
471 }
472
473 if let Some(node_volume_size) = options.node_volume_size {
474 args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
475 }
476
477 if let Some(peer_cache_image_id) = &options.peer_cache_image_id {
478 args.push((
479 "peer_cache_droplet_image_id".to_string(),
480 peer_cache_image_id.clone(),
481 ));
482 }
483
484 if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
485 args.push((
486 "peer_cache_node_vm_count".to_string(),
487 peer_cache_node_vm_count.to_string(),
488 ));
489 }
490
491 if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
492 args.push((
493 "peer_cache_droplet_size".to_string(),
494 peer_cache_vm_size.clone(),
495 ));
496 }
497
498 if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
499 args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
500 }
501
502 if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
503 args.push((
504 "peer_cache_node_volume_size".to_string(),
505 peer_cache_node_volume_size.to_string(),
506 ));
507 }
508
509 if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
510 args.push((
511 "symmetric_nat_gateway_droplet_size".to_string(),
512 nat_gateway_vm_size.clone(),
513 ));
514 }
515
516 if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
517 args.push((
518 "symmetric_private_node_vm_count".to_string(),
519 symmetric_private_node_vm_count.to_string(),
520 ));
521 }
522
523 if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
524 args.push((
525 "symmetric_private_node_volume_size".to_string(),
526 symmetric_private_node_volume_size.to_string(),
527 ));
528 }
529
530 Ok(args)
531}
532
533pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
536 terraform_runner.init()?;
537 let workspaces = terraform_runner.workspace_list()?;
538 if !workspaces.contains(&name.to_string()) {
539 return Err(Error::EnvironmentDoesNotExist(name.to_string()));
540 }
541 terraform_runner.workspace_select(name)?;
542 println!("Selected {name} workspace");
543 Ok(())
544}
545
546pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
547 terraform_runner.workspace_select("dev")?;
551 terraform_runner.workspace_delete(name)?;
552 println!("Deleted {name} workspace");
553 Ok(())
554}
555
556fn get_value_for_resource<T>(
558 resources: &[TerraformResource],
559 resource_name: &str,
560 field_name: &str,
561) -> Result<Option<T>, Error>
562where
563 T: From<TerraformValue>,
564{
565 let field_value = resources
566 .iter()
567 .filter(|r| r.resource_name == resource_name)
568 .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
569 if let Some(value) = r.values.get(field_name) {
570 match acc_value {
571 Some(ref existing_value) if existing_value != value => {
572 log::error!("Expected value: {existing_value}, got value: {value}");
573 Err(Error::TerraformResourceValueMismatch {
574 expected: existing_value.to_string(),
575 actual: value.to_string(),
576 })
577 }
578 _ => Ok(Some(value.clone())),
579 }
580 } else {
581 Ok(acc_value)
582 }
583 })?;
584
585 Ok(field_value.map(TerraformValue::from).map(T::from))
586}
587
588#[derive(Debug, Clone)]
590enum TerraformValue {
591 String(String),
592 Number(u64),
593 Bool(bool),
594 Other(serde_json::Value),
595}
596
597impl From<serde_json::Value> for TerraformValue {
598 fn from(value: serde_json::Value) -> Self {
599 if value.is_string() {
600 TerraformValue::String(value.as_str().unwrap().to_string())
603 } else if value.is_u64() {
604 TerraformValue::Number(value.as_u64().unwrap())
606 } else if value.is_boolean() {
607 TerraformValue::Bool(value.as_bool().unwrap())
609 } else {
610 TerraformValue::Other(value)
611 }
612 }
613}
614
615impl From<TerraformValue> for String {
617 fn from(value: TerraformValue) -> Self {
618 match value {
619 TerraformValue::String(s) => s,
620 TerraformValue::Number(n) => n.to_string(),
621 TerraformValue::Bool(b) => b.to_string(),
622 TerraformValue::Other(v) => v.to_string(),
623 }
624 }
625}
626
627impl From<TerraformValue> for u16 {
628 fn from(value: TerraformValue) -> Self {
629 match value {
630 TerraformValue::Number(n) => n as u16,
631 TerraformValue::String(s) => s.parse().unwrap_or(0),
632 _ => 0,
633 }
634 }
635}