sn_testnet_deploy/
infra.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use 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    /// Set to None for new deployments, as the value will be fetched from tfvars.
42    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    /// Set to None for new deployments, as the value will be fetched from tfvars.
50    pub nat_gateway_image_id: Option<String>,
51    /// Set to None for new deployments, as the value will be fetched from tfvars.
52    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    /// Set to None for new deployments, as the value will be fetched from tfvars.
57    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    /// Set to None for new deployments, as the value will be fetched from tfvars.
66    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    /// Generate the options for an existing deployment.
73    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                // gateways should exists if private nodes exist
146                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                // gateways should exists if private nodes exist
182                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        // Node VM size var is re-used for nodes, evm nodes, symmetric and full cone private nodes
221        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    /// Create or update the infrastructure for a deployment.
299    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    /// Set to None for new deployments, as the value will be fetched from tfvars.
322    pub uploader_image_id: Option<String>,
323}
324
325impl UploaderInfraRunOptions {
326    /// Generate the options for an existing uploader deployment.
327    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
404/// Extract a specific field value from terraform resources.
405fn 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
433/// Build the terraform arguments from InfraRunOptions
434pub 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
592/// Select a Terraform workspace for an environment.
593/// Returns an error if the environment doesn't exist.
594pub 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    // The 'dev' workspace is one we always expect to exist, for admin purposes.
607    // You can't delete a workspace while it is selected, so we select 'dev' before we delete
608    // the current workspace.
609    terraform_runner.workspace_select("dev")?;
610    terraform_runner.workspace_delete(name)?;
611    println!("Deleted {name} workspace");
612    Ok(())
613}