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 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    /// Generate the options for an existing deployment.
42    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        // There will always be a genesis node in a new deployment, but none in a bootstrap deployment.
82        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                // gateways should exists if private nodes exist
133                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                // gateways should exists if private nodes exist
167                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        // Node VM size var is re-used for nodes, evm nodes, symmetric and full cone private nodes
205        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            // The EVM node size never needs to change so it will be obtained from the tfvars file
251            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    /// Create or update the infrastructure for a deployment.
280    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    /// Generate the options for an existing uploader deployment.
306    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
375/// Extract a specific field value from terraform resources.
376fn 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
404/// Build the terraform arguments from InfraRunOptions
405pub 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
525/// Select a Terraform workspace for an environment.
526/// Returns an error if the environment doesn't exist.
527pub 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    // The 'dev' workspace is one we always expect to exist, for admin purposes.
540    // You can't delete a workspace while it is selected, so we select 'dev' before we delete
541    // the current workspace.
542    terraform_runner.workspace_select("dev")?;
543    terraform_runner.workspace_delete(name)?;
544    println!("Deleted {name} workspace");
545    Ok(())
546}