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    fn get_value_for_resource(
42        resources: &[TerraformResource],
43        resource_name: &str,
44        field_name: &str,
45    ) -> Result<serde_json::Value, Error> {
46        let field_value = resources
47            .iter()
48            .filter(|r| r.resource_name == resource_name)
49            .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
50                let Some(value) = r.values.get(field_name) else {
51                    log::error!("Failed to obtain '{field_name}' value for {resource_name}");
52                    return Err(Error::TerraformResourceFieldMissing(field_name.to_string()));
53                };
54                match acc_value {
55                    Some(ref existing_value) if existing_value != value => {
56                        log::error!("Expected value: {existing_value}, got value: {value}");
57                        Err(Error::TerraformResourceValueMismatch {
58                            expected: existing_value.to_string(),
59                            actual: value.to_string(),
60                        })
61                    }
62                    _ => Ok(Some(value.clone())),
63                }
64            })?;
65
66        field_value.ok_or(Error::TerraformResourceFieldMissing(field_name.to_string()))
67    }
68
69    /// Generate the options for an existing deployment.
70    pub async fn generate_existing(
71        name: &str,
72        terraform_runner: &TerraformRunner,
73        environment_details: &EnvironmentDetails,
74    ) -> Result<Self> {
75        let resources = terraform_runner.show(name)?;
76
77        let resource_count = |resource_name: &str| -> u16 {
78            resources
79                .iter()
80                .filter(|r| r.resource_name == resource_name)
81                .count() as u16
82        };
83
84        let peer_cache_node_vm_count = resource_count("peer_cache_node");
85        let (peer_cache_node_volume_size, peer_cache_node_vm_size) = if peer_cache_node_vm_count > 0
86        {
87            let volume_size = Self::get_value_for_resource(
88                &resources,
89                "peer_cache_node_attached_volume",
90                "size",
91            )?
92            .as_u64()
93            .ok_or_else(|| {
94                log::error!(
95                    "Failed to obtain u64 'size' value for peer_cache_node_attached_volume"
96                );
97                Error::TerraformResourceFieldMissing("size".to_string())
98            })?;
99            let vm_size = Self::get_value_for_resource(&resources, "peer_cache_node", "size")?
100                .as_str()
101                .ok_or_else(|| {
102                    log::error!("Failed to obtain str 'size' value for peer_cache_node");
103                    Error::TerraformResourceFieldMissing("size".to_string())
104                })?
105                .to_string();
106
107            (Some(volume_size as u16), Some(vm_size))
108        } else {
109            (None, None)
110        };
111
112        // There will always be a genesis node in a new deployment, but none in a bootstrap deployment.
113        let genesis_vm_count = match environment_details.deployment_type {
114            DeploymentType::New => 1,
115            DeploymentType::Bootstrap => 0,
116        };
117        let genesis_node_volume_size = if genesis_vm_count > 0 {
118            let genesis_node_volume_size =
119                Self::get_value_for_resource(&resources, "genesis_node_attached_volume", "size")?
120                    .as_u64()
121                    .ok_or_else(|| {
122                        log::error!(
123                            "Failed to obtain u64 'size' value for genesis_node_attached_volume"
124                        );
125                        Error::TerraformResourceFieldMissing("size".to_string())
126                    })?;
127            Some(genesis_node_volume_size as u16)
128        } else {
129            None
130        };
131
132        let node_vm_count = resource_count("node");
133        let node_volume_size = if node_vm_count > 0 {
134            let node_volume_size =
135                Self::get_value_for_resource(&resources, "node_attached_volume", "size")?
136                    .as_u64()
137                    .ok_or_else(|| {
138                        log::error!("Failed to obtain u64 'size' value for node_attached_volume");
139                        Error::TerraformResourceFieldMissing("size".to_string())
140                    })?;
141
142            Some(node_volume_size as u16)
143        } else {
144            None
145        };
146
147        let symmetric_private_node_vm_count = resource_count("symmetric_private_node");
148        let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
149            if symmetric_private_node_vm_count > 0 {
150                let symmetric_private_node_volume_size = Self::get_value_for_resource(
151                    &resources,
152                    "symmetric_private_node_attached_volume",
153                    "size",
154                )?
155                .as_u64()
156                .ok_or_else(|| {
157                    log::error!(
158                    "Failed to obtain u64 'size' value for symmetric_private_node_attached_volume"
159                );
160                    Error::TerraformResourceFieldMissing("size".to_string())
161                })?;
162                // gateways should exists if private nodes exist
163                let symmetric_nat_gateway_vm_size =
164                    Self::get_value_for_resource(&resources, "symmetric_nat_gateway", "size")?
165                        .as_str()
166                        .ok_or_else(|| {
167                            log::error!(
168                                "Failed to obtain str 'size' value for symmetric_nat_gateway"
169                            );
170                            Error::TerraformResourceFieldMissing("size".to_string())
171                        })?
172                        .to_string();
173
174                (
175                    Some(symmetric_private_node_volume_size as u16),
176                    Some(symmetric_nat_gateway_vm_size),
177                )
178            } else {
179                (None, None)
180            };
181        let full_cone_private_node_vm_count = resource_count("full_cone_private_node");
182        let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
183            if full_cone_private_node_vm_count > 0 {
184                let full_cone_private_node_volume_size = Self::get_value_for_resource(
185                    &resources,
186                    "full_cone_private_node_attached_volume",
187                    "size",
188                )?
189                .as_u64()
190                .ok_or_else(|| {
191                    log::error!(
192                    "Failed to obtain u64 'size' value for full_cone_private_node_attached_volume"
193                );
194                    Error::TerraformResourceFieldMissing("size".to_string())
195                })?;
196                // gateways should exists if private nodes exist
197                let full_cone_nat_gateway_vm_size =
198                    Self::get_value_for_resource(&resources, "full_cone_nat_gateway", "size")?
199                        .as_str()
200                        .ok_or_else(|| {
201                            log::error!(
202                                "Failed to obtain str 'size' value for full_cone_nat_gateway"
203                            );
204                            Error::TerraformResourceFieldMissing("size".to_string())
205                        })?
206                        .to_string();
207
208                (
209                    Some(full_cone_private_node_volume_size as u16),
210                    Some(full_cone_nat_gateway_vm_size),
211                )
212            } else {
213                (None, None)
214            };
215
216        let uploader_vm_count = resource_count("uploader");
217        let uploader_vm_size = if uploader_vm_count > 0 {
218            let uploader_vm_size = Self::get_value_for_resource(&resources, "uploader", "size")?
219                .as_str()
220                .ok_or_else(|| {
221                    log::error!("Failed to obtain str 'size' value for uploader");
222                    Error::TerraformResourceFieldMissing("size".to_string())
223                })?
224                .to_string();
225            Some(uploader_vm_size)
226        } else {
227            None
228        };
229
230        let evm_node_count = resource_count("evm_node");
231        let build_vm_count = resource_count("build");
232        let enable_build_vm = build_vm_count > 0;
233
234        // Node VM size var is re-used for nodes, evm nodes, symmetric and full cone private nodes
235        let node_vm_size = if node_vm_count > 0 {
236            let node_vm_size = Self::get_value_for_resource(&resources, "node", "size")?
237                .as_str()
238                .ok_or_else(|| {
239                    log::error!("Failed to obtain str 'size' value for node");
240                    Error::TerraformResourceFieldMissing("size".to_string())
241                })?
242                .to_string();
243            Some(node_vm_size)
244        } else if symmetric_private_node_vm_count > 0 {
245            let symmetric_private_node_vm_size =
246                Self::get_value_for_resource(&resources, "symmetric_private_node", "size")?
247                    .as_str()
248                    .ok_or_else(|| {
249                        log::error!("Failed to obtain str 'size' value for symmetric_private_node");
250                        Error::TerraformResourceFieldMissing("size".to_string())
251                    })?
252                    .to_string();
253            Some(symmetric_private_node_vm_size)
254        } else if full_cone_private_node_vm_count > 0 {
255            let full_cone_private_node_vm_size =
256                Self::get_value_for_resource(&resources, "full_cone_private_node", "size")?
257                    .as_str()
258                    .ok_or_else(|| {
259                        log::error!("Failed to obtain str 'size' value for full_cone_private_node");
260                        Error::TerraformResourceFieldMissing("size".to_string())
261                    })?
262                    .to_string();
263            Some(full_cone_private_node_vm_size)
264        } else if evm_node_count > 0 {
265            let evm_node_vm_size = Self::get_value_for_resource(&resources, "evm_node", "size")?
266                .as_str()
267                .ok_or_else(|| {
268                    log::error!("Failed to obtain str 'size' value for evm_node");
269                    Error::TerraformResourceFieldMissing("size".to_string())
270                })?
271                .to_string();
272            Some(evm_node_vm_size)
273        } else {
274            None
275        };
276
277        let options = Self {
278            enable_build_vm,
279            evm_node_count: Some(evm_node_count),
280            // The EVM node size never needs to change so it will be obtained from the tfvars file
281            evm_node_vm_size: None,
282            full_cone_nat_gateway_vm_size,
283            full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
284            full_cone_private_node_volume_size,
285            genesis_vm_count: Some(genesis_vm_count),
286            genesis_node_volume_size,
287            name: name.to_string(),
288            node_vm_count: Some(node_vm_count),
289            node_vm_size,
290            node_volume_size,
291            peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
292            peer_cache_node_vm_size,
293            peer_cache_node_volume_size,
294            symmetric_nat_gateway_vm_size,
295            symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
296            symmetric_private_node_volume_size,
297            tfvars_filename: environment_details
298                .environment_type
299                .get_tfvars_filename(name),
300            uploader_vm_count: Some(uploader_vm_count),
301            uploader_vm_size,
302        };
303
304        Ok(options)
305    }
306}
307
308impl TestnetDeployer {
309    /// Create or update the infrastructure for a deployment.
310    pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
311        let start = Instant::now();
312        println!("Selecting {} workspace...", options.name);
313        self.terraform_runner.workspace_select(&options.name)?;
314
315        let args = build_terraform_args(options)?;
316
317        println!("Running terraform apply...");
318        self.terraform_runner
319            .apply(args, Some(options.tfvars_filename.clone()))?;
320        print_duration(start.elapsed());
321        Ok(())
322    }
323}
324
325/// Build the terraform arguments from InfraRunOptions
326pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
327    let mut args = Vec::new();
328
329    if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
330        args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
331    }
332
333    if let Some(genesis_vm_count) = options.genesis_vm_count {
334        args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
335    }
336
337    if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
338        args.push((
339            "peer_cache_node_vm_count".to_string(),
340            peer_cache_node_vm_count.to_string(),
341        ));
342    }
343    if let Some(node_vm_count) = options.node_vm_count {
344        args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
345    }
346
347    if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
348        args.push((
349            "symmetric_private_node_vm_count".to_string(),
350            symmetric_private_node_vm_count.to_string(),
351        ));
352    }
353    if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
354        args.push((
355            "full_cone_private_node_vm_count".to_string(),
356            full_cone_private_node_vm_count.to_string(),
357        ));
358    }
359
360    if let Some(evm_node_count) = options.evm_node_count {
361        args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
362    }
363
364    if let Some(uploader_vm_count) = options.uploader_vm_count {
365        args.push((
366            "uploader_vm_count".to_string(),
367            uploader_vm_count.to_string(),
368        ));
369    }
370
371    args.push((
372        "use_custom_bin".to_string(),
373        options.enable_build_vm.to_string(),
374    ));
375
376    if let Some(node_vm_size) = &options.node_vm_size {
377        args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
378    }
379
380    if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
381        args.push((
382            "peer_cache_droplet_size".to_string(),
383            peer_cache_vm_size.clone(),
384        ));
385    }
386
387    if let Some(uploader_vm_size) = &options.uploader_vm_size {
388        args.push((
389            "uploader_droplet_size".to_string(),
390            uploader_vm_size.clone(),
391        ));
392    }
393
394    if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
395        args.push((
396            "evm_node_droplet_size".to_string(),
397            evm_node_vm_size.clone(),
398        ));
399    }
400
401    if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
402        args.push((
403            "peer_cache_node_volume_size".to_string(),
404            peer_cache_node_volume_size.to_string(),
405        ));
406    }
407    if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
408        args.push((
409            "genesis_node_volume_size".to_string(),
410            genesis_node_volume_size.to_string(),
411        ));
412    }
413    if let Some(node_volume_size) = options.node_volume_size {
414        args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
415    }
416
417    if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
418        args.push((
419            "full_cone_nat_gateway_droplet_size".to_string(),
420            full_cone_gateway_vm_size.clone(),
421        ));
422    }
423    if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
424        args.push((
425            "full_cone_private_node_volume_size".to_string(),
426            full_cone_private_node_volume_size.to_string(),
427        ));
428    }
429
430    if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
431        args.push((
432            "symmetric_nat_gateway_droplet_size".to_string(),
433            nat_gateway_vm_size.clone(),
434        ));
435    }
436    if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
437        args.push((
438            "symmetric_private_node_volume_size".to_string(),
439            symmetric_private_node_volume_size.to_string(),
440        ));
441    }
442
443    Ok(args)
444}