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