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 pub async fn generate_existing(
43 name: &str,
44 terraform_runner: &TerraformRunner,
45 environment_details: &EnvironmentDetails,
46 ) -> Result<Self> {
47 let resources = terraform_runner.show(name)?;
48
49 let resource_count = |resource_name: &str| -> u16 {
50 resources
51 .iter()
52 .filter(|r| r.resource_name == resource_name)
53 .count() as u16
54 };
55
56 let peer_cache_node_vm_count = resource_count("peer_cache_node");
57 let (peer_cache_node_volume_size, peer_cache_node_vm_size) = if peer_cache_node_vm_count > 0
58 {
59 let volume_size =
60 get_value_for_resource(&resources, "peer_cache_node_attached_volume", "size")?
61 .as_u64()
62 .ok_or_else(|| {
63 log::error!(
64 "Failed to obtain u64 'size' value for peer_cache_node_attached_volume"
65 );
66 Error::TerraformResourceFieldMissing("size".to_string())
67 })?;
68 let vm_size = get_value_for_resource(&resources, "peer_cache_node", "size")?
69 .as_str()
70 .ok_or_else(|| {
71 log::error!("Failed to obtain str 'size' value for peer_cache_node");
72 Error::TerraformResourceFieldMissing("size".to_string())
73 })?
74 .to_string();
75
76 (Some(volume_size as u16), Some(vm_size))
77 } else {
78 (None, None)
79 };
80
81 let genesis_vm_count = match environment_details.deployment_type {
83 DeploymentType::New => 1,
84 DeploymentType::Bootstrap => 0,
85 DeploymentType::Uploaders => 0,
86 };
87 let genesis_node_volume_size = if genesis_vm_count > 0 {
88 let genesis_node_volume_size =
89 get_value_for_resource(&resources, "genesis_node_attached_volume", "size")?
90 .as_u64()
91 .ok_or_else(|| {
92 log::error!(
93 "Failed to obtain u64 'size' value for genesis_node_attached_volume"
94 );
95 Error::TerraformResourceFieldMissing("size".to_string())
96 })?;
97 Some(genesis_node_volume_size as u16)
98 } else {
99 None
100 };
101
102 let node_vm_count = resource_count("node");
103 let node_volume_size = if node_vm_count > 0 {
104 let node_volume_size =
105 get_value_for_resource(&resources, "node_attached_volume", "size")?
106 .as_u64()
107 .ok_or_else(|| {
108 log::error!("Failed to obtain u64 'size' value for node_attached_volume");
109 Error::TerraformResourceFieldMissing("size".to_string())
110 })?;
111
112 Some(node_volume_size as u16)
113 } else {
114 None
115 };
116
117 let symmetric_private_node_vm_count = resource_count("symmetric_private_node");
118 let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
119 if symmetric_private_node_vm_count > 0 {
120 let symmetric_private_node_volume_size = get_value_for_resource(
121 &resources,
122 "symmetric_private_node_attached_volume",
123 "size",
124 )?
125 .as_u64()
126 .ok_or_else(|| {
127 log::error!(
128 "Failed to obtain u64 'size' value for symmetric_private_node_attached_volume"
129 );
130 Error::TerraformResourceFieldMissing("size".to_string())
131 })?;
132 let symmetric_nat_gateway_vm_size =
134 get_value_for_resource(&resources, "symmetric_nat_gateway", "size")?
135 .as_str()
136 .ok_or_else(|| {
137 log::error!(
138 "Failed to obtain str 'size' value for symmetric_nat_gateway"
139 );
140 Error::TerraformResourceFieldMissing("size".to_string())
141 })?
142 .to_string();
143
144 (
145 Some(symmetric_private_node_volume_size as u16),
146 Some(symmetric_nat_gateway_vm_size),
147 )
148 } else {
149 (None, None)
150 };
151 let full_cone_private_node_vm_count = resource_count("full_cone_private_node");
152 let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
153 if full_cone_private_node_vm_count > 0 {
154 let full_cone_private_node_volume_size = get_value_for_resource(
155 &resources,
156 "full_cone_private_node_attached_volume",
157 "size",
158 )?
159 .as_u64()
160 .ok_or_else(|| {
161 log::error!(
162 "Failed to obtain u64 'size' value for full_cone_private_node_attached_volume"
163 );
164 Error::TerraformResourceFieldMissing("size".to_string())
165 })?;
166 let full_cone_nat_gateway_vm_size =
168 get_value_for_resource(&resources, "full_cone_nat_gateway", "size")?
169 .as_str()
170 .ok_or_else(|| {
171 log::error!(
172 "Failed to obtain str 'size' value for full_cone_nat_gateway"
173 );
174 Error::TerraformResourceFieldMissing("size".to_string())
175 })?
176 .to_string();
177
178 (
179 Some(full_cone_private_node_volume_size as u16),
180 Some(full_cone_nat_gateway_vm_size),
181 )
182 } else {
183 (None, None)
184 };
185
186 let uploader_vm_count = resource_count("uploader");
187 let uploader_vm_size = if uploader_vm_count > 0 {
188 let uploader_vm_size = get_value_for_resource(&resources, "uploader", "size")?
189 .as_str()
190 .ok_or_else(|| {
191 log::error!("Failed to obtain str 'size' value for uploader");
192 Error::TerraformResourceFieldMissing("size".to_string())
193 })?
194 .to_string();
195 Some(uploader_vm_size)
196 } else {
197 None
198 };
199
200 let evm_node_count = resource_count("evm_node");
201 let build_vm_count = resource_count("build");
202 let enable_build_vm = build_vm_count > 0;
203
204 let node_vm_size = if node_vm_count > 0 {
206 let node_vm_size = get_value_for_resource(&resources, "node", "size")?
207 .as_str()
208 .ok_or_else(|| {
209 log::error!("Failed to obtain str 'size' value for node");
210 Error::TerraformResourceFieldMissing("size".to_string())
211 })?
212 .to_string();
213 Some(node_vm_size)
214 } else if symmetric_private_node_vm_count > 0 {
215 let symmetric_private_node_vm_size =
216 get_value_for_resource(&resources, "symmetric_private_node", "size")?
217 .as_str()
218 .ok_or_else(|| {
219 log::error!("Failed to obtain str 'size' value for symmetric_private_node");
220 Error::TerraformResourceFieldMissing("size".to_string())
221 })?
222 .to_string();
223 Some(symmetric_private_node_vm_size)
224 } else if full_cone_private_node_vm_count > 0 {
225 let full_cone_private_node_vm_size =
226 get_value_for_resource(&resources, "full_cone_private_node", "size")?
227 .as_str()
228 .ok_or_else(|| {
229 log::error!("Failed to obtain str 'size' value for full_cone_private_node");
230 Error::TerraformResourceFieldMissing("size".to_string())
231 })?
232 .to_string();
233 Some(full_cone_private_node_vm_size)
234 } else if evm_node_count > 0 {
235 let evm_node_vm_size = get_value_for_resource(&resources, "evm_node", "size")?
236 .as_str()
237 .ok_or_else(|| {
238 log::error!("Failed to obtain str 'size' value for evm_node");
239 Error::TerraformResourceFieldMissing("size".to_string())
240 })?
241 .to_string();
242 Some(evm_node_vm_size)
243 } else {
244 None
245 };
246
247 let options = Self {
248 enable_build_vm,
249 evm_node_count: Some(evm_node_count),
250 evm_node_vm_size: None,
252 full_cone_nat_gateway_vm_size,
253 full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
254 full_cone_private_node_volume_size,
255 genesis_vm_count: Some(genesis_vm_count),
256 genesis_node_volume_size,
257 name: name.to_string(),
258 node_vm_count: Some(node_vm_count),
259 node_vm_size,
260 node_volume_size,
261 peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
262 peer_cache_node_vm_size,
263 peer_cache_node_volume_size,
264 symmetric_nat_gateway_vm_size,
265 symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
266 symmetric_private_node_volume_size,
267 tfvars_filename: environment_details
268 .environment_type
269 .get_tfvars_filename(name),
270 uploader_vm_count: Some(uploader_vm_count),
271 uploader_vm_size,
272 };
273
274 Ok(options)
275 }
276}
277
278impl TestnetDeployer {
279 pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
281 let start = Instant::now();
282 println!("Selecting {} workspace...", options.name);
283 self.terraform_runner.workspace_select(&options.name)?;
284
285 let args = build_terraform_args(options)?;
286
287 println!("Running terraform apply...");
288 self.terraform_runner
289 .apply(args, Some(options.tfvars_filename.clone()))?;
290 print_duration(start.elapsed());
291 Ok(())
292 }
293}
294
295#[derive(Clone, Debug)]
296pub struct UploaderInfraRunOptions {
297 pub enable_build_vm: bool,
298 pub name: String,
299 pub tfvars_filename: String,
300 pub uploader_vm_count: Option<u16>,
301 pub uploader_vm_size: Option<String>,
302}
303
304impl UploaderInfraRunOptions {
305 pub async fn generate_existing(
307 name: &str,
308 terraform_runner: &TerraformRunner,
309 environment_details: &EnvironmentDetails,
310 ) -> Result<Self> {
311 let resources = terraform_runner.show(name)?;
312
313 let resource_count = |resource_name: &str| -> u16 {
314 resources
315 .iter()
316 .filter(|r| r.resource_name == resource_name)
317 .count() as u16
318 };
319
320 let uploader_vm_count = resource_count("uploader");
321 let uploader_vm_size = if uploader_vm_count > 0 {
322 let uploader_vm_size = get_value_for_resource(&resources, "uploader", "size")?
323 .as_str()
324 .ok_or_else(|| {
325 log::error!("Failed to obtain str 'size' value for uploader");
326 Error::TerraformResourceFieldMissing("size".to_string())
327 })?
328 .to_string();
329 Some(uploader_vm_size)
330 } else {
331 None
332 };
333
334 let build_vm_count = resource_count("build");
335 let enable_build_vm = build_vm_count > 0;
336
337 let options = Self {
338 enable_build_vm,
339 name: name.to_string(),
340 tfvars_filename: environment_details
341 .environment_type
342 .get_tfvars_filename(name),
343 uploader_vm_count: Some(uploader_vm_count),
344 uploader_vm_size,
345 };
346
347 Ok(options)
348 }
349
350 pub fn build_terraform_args(&self) -> Result<Vec<(String, String)>> {
351 let mut args = Vec::new();
352
353 args.push((
354 "use_custom_bin".to_string(),
355 self.enable_build_vm.to_string(),
356 ));
357
358 if let Some(uploader_vm_count) = self.uploader_vm_count {
359 args.push((
360 "uploader_vm_count".to_string(),
361 uploader_vm_count.to_string(),
362 ));
363 }
364 if let Some(uploader_vm_size) = &self.uploader_vm_size {
365 args.push((
366 "uploader_droplet_size".to_string(),
367 uploader_vm_size.clone(),
368 ));
369 }
370
371 Ok(args)
372 }
373}
374
375fn get_value_for_resource(
377 resources: &[TerraformResource],
378 resource_name: &str,
379 field_name: &str,
380) -> Result<serde_json::Value, Error> {
381 let field_value = resources
382 .iter()
383 .filter(|r| r.resource_name == resource_name)
384 .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
385 let Some(value) = r.values.get(field_name) else {
386 log::error!("Failed to obtain '{field_name}' value for {resource_name}");
387 return Err(Error::TerraformResourceFieldMissing(field_name.to_string()));
388 };
389 match acc_value {
390 Some(ref existing_value) if existing_value != value => {
391 log::error!("Expected value: {existing_value}, got value: {value}");
392 Err(Error::TerraformResourceValueMismatch {
393 expected: existing_value.to_string(),
394 actual: value.to_string(),
395 })
396 }
397 _ => Ok(Some(value.clone())),
398 }
399 })?;
400
401 field_value.ok_or(Error::TerraformResourceFieldMissing(field_name.to_string()))
402}
403
404pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
406 let mut args = Vec::new();
407
408 if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
409 args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
410 }
411
412 if let Some(genesis_vm_count) = options.genesis_vm_count {
413 args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
414 }
415
416 if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
417 args.push((
418 "peer_cache_node_vm_count".to_string(),
419 peer_cache_node_vm_count.to_string(),
420 ));
421 }
422 if let Some(node_vm_count) = options.node_vm_count {
423 args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
424 }
425
426 if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
427 args.push((
428 "symmetric_private_node_vm_count".to_string(),
429 symmetric_private_node_vm_count.to_string(),
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(evm_node_count) = options.evm_node_count {
440 args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
441 }
442
443 if let Some(uploader_vm_count) = options.uploader_vm_count {
444 args.push((
445 "uploader_vm_count".to_string(),
446 uploader_vm_count.to_string(),
447 ));
448 }
449
450 args.push((
451 "use_custom_bin".to_string(),
452 options.enable_build_vm.to_string(),
453 ));
454
455 if let Some(node_vm_size) = &options.node_vm_size {
456 args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
457 }
458
459 if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
460 args.push((
461 "peer_cache_droplet_size".to_string(),
462 peer_cache_vm_size.clone(),
463 ));
464 }
465
466 if let Some(uploader_vm_size) = &options.uploader_vm_size {
467 args.push((
468 "uploader_droplet_size".to_string(),
469 uploader_vm_size.clone(),
470 ));
471 }
472
473 if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
474 args.push((
475 "evm_node_droplet_size".to_string(),
476 evm_node_vm_size.clone(),
477 ));
478 }
479
480 if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
481 args.push((
482 "peer_cache_node_volume_size".to_string(),
483 peer_cache_node_volume_size.to_string(),
484 ));
485 }
486 if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
487 args.push((
488 "genesis_node_volume_size".to_string(),
489 genesis_node_volume_size.to_string(),
490 ));
491 }
492 if let Some(node_volume_size) = options.node_volume_size {
493 args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
494 }
495
496 if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
497 args.push((
498 "full_cone_nat_gateway_droplet_size".to_string(),
499 full_cone_gateway_vm_size.clone(),
500 ));
501 }
502 if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
503 args.push((
504 "full_cone_private_node_volume_size".to_string(),
505 full_cone_private_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 if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
516 args.push((
517 "symmetric_private_node_volume_size".to_string(),
518 symmetric_private_node_volume_size.to_string(),
519 ));
520 }
521
522 Ok(args)
523}
524
525pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
528 terraform_runner.init()?;
529 let workspaces = terraform_runner.workspace_list()?;
530 if !workspaces.contains(&name.to_string()) {
531 return Err(Error::EnvironmentDoesNotExist(name.to_string()));
532 }
533 terraform_runner.workspace_select(name)?;
534 println!("Selected {name} workspace");
535 Ok(())
536}
537
538pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
539 terraform_runner.workspace_select("dev")?;
543 terraform_runner.workspace_delete(name)?;
544 println!("Deleted {name} workspace");
545 Ok(())
546}