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                debug!("Peer cache node volume size: {volume_size:?}");
94                let vm_size = get_value_for_resource(&resources, PEER_CACHE_NODE, SIZE)?;
95                debug!("Peer cache node size: {vm_size:?}");
96                let image_id = get_value_for_resource(&resources, PEER_CACHE_NODE, IMAGE)?;
97                debug!("Peer cache node image id: {image_id:?}");
98
99                (volume_size, vm_size, image_id)
100            } else {
101                (None, None, None)
102            };
103
104        let genesis_node_vm_count = resource_count(GENESIS_NODE);
105        debug!("Genesis node count: {genesis_node_vm_count}");
106        let genesis_node_volume_size = if genesis_node_vm_count > 0 {
107            get_value_for_resource(&resources, GENESIS_NODE_ATTACHED_VOLUME, SIZE)?
108        } else {
109            None
110        };
111        debug!("Genesis node volume size: {genesis_node_volume_size:?}");
112
113        let node_vm_count = resource_count(NODE);
114        debug!("Node count: {node_vm_count}");
115        let node_volume_size = if node_vm_count > 0 {
116            get_value_for_resource(&resources, NODE_ATTACHED_VOLUME, SIZE)?
117        } else {
118            None
119        };
120        debug!("Node volume size: {node_volume_size:?}");
121
122        let mut nat_gateway_image_id: Option<String> = None;
123        let symmetric_private_node_vm_count = resource_count(SYMMETRIC_PRIVATE_NODE);
124        debug!("Symmetric private node count: {symmetric_private_node_vm_count}");
125        let (symmetric_private_node_volume_size, symmetric_nat_gateway_vm_size) =
126            if symmetric_private_node_vm_count > 0 {
127                let symmetric_private_node_volume_size = get_value_for_resource(
128                    &resources,
129                    SYMMETRIC_PRIVATE_NODE_ATTACHED_VOLUME,
130                    SIZE,
131                )?;
132                debug!(
133                    "Symmetric private node volume size: {symmetric_private_node_volume_size:?}"
134                );
135                // gateways should exists if private nodes exist
136                let symmetric_nat_gateway_vm_size =
137                    get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, SIZE)?;
138
139                debug!("Symmetric nat gateway size: {symmetric_nat_gateway_vm_size:?}");
140
141                nat_gateway_image_id =
142                    get_value_for_resource(&resources, SYMMETRIC_NAT_GATEWAY, IMAGE)?;
143                debug!("Nat gateway image: {nat_gateway_image_id:?}");
144
145                (
146                    symmetric_private_node_volume_size,
147                    symmetric_nat_gateway_vm_size,
148                )
149            } else {
150                (None, None)
151            };
152
153        let full_cone_private_node_vm_count = resource_count(FULL_CONE_PRIVATE_NODE);
154        debug!("Full cone private node count: {full_cone_private_node_vm_count}");
155        let (full_cone_private_node_volume_size, full_cone_nat_gateway_vm_size) =
156            if full_cone_private_node_vm_count > 0 {
157                let full_cone_private_node_volume_size = get_value_for_resource(
158                    &resources,
159                    FULL_CONE_PRIVATE_NODE_ATTACHED_VOLUME,
160                    SIZE,
161                )?;
162                debug!(
163                    "Full cone private node volume size: {full_cone_private_node_volume_size:?}"
164                );
165                // gateways should exists if private nodes exist
166                let full_cone_nat_gateway_vm_size =
167                    get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, SIZE)?;
168                debug!("Full cone nat gateway size: {full_cone_nat_gateway_vm_size:?}");
169
170                nat_gateway_image_id =
171                    get_value_for_resource(&resources, FULL_CONE_NAT_GATEWAY, IMAGE)?;
172                debug!("Nat gateway image: {nat_gateway_image_id:?}");
173
174                (
175                    full_cone_private_node_volume_size,
176                    full_cone_nat_gateway_vm_size,
177                )
178            } else {
179                (None, None)
180            };
181
182        let uploader_vm_count = resource_count(UPLOADER);
183        debug!("Uploader count: {uploader_vm_count}");
184        let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
185            let vm_size = get_value_for_resource(&resources, UPLOADER, SIZE)?;
186            debug!("Uploader size: {vm_size:?}");
187            let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?;
188            debug!("Uploader image id: {image_id:?}");
189            (vm_size, image_id)
190        } else {
191            (None, None)
192        };
193
194        let build_vm_count = resource_count(BUILD_VM);
195        debug!("Build VM count: {build_vm_count}");
196        let enable_build_vm = build_vm_count > 0;
197
198        // Node VM size var is re-used for nodes, evm nodes, symmetric and full cone private nodes
199        let (node_vm_size, node_image_id) = if node_vm_count > 0 {
200            let vm_size = get_value_for_resource(&resources, NODE, SIZE)?;
201            debug!("Node size obtained from {NODE}: {vm_size:?}");
202            let image_id = get_value_for_resource(&resources, NODE, IMAGE)?;
203            debug!("Node image id obtained from {NODE}: {image_id:?}");
204            (vm_size, image_id)
205        } else if symmetric_private_node_vm_count > 0 {
206            let vm_size = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, SIZE)?;
207            debug!("Node size obtained from {SYMMETRIC_PRIVATE_NODE}: {vm_size:?}");
208            let image_id = get_value_for_resource(&resources, SYMMETRIC_PRIVATE_NODE, IMAGE)?;
209            debug!("Node image id obtained from {SYMMETRIC_PRIVATE_NODE}: {image_id:?}");
210            (vm_size, image_id)
211        } else if full_cone_private_node_vm_count > 0 {
212            let vm_size = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, SIZE)?;
213            debug!("Node size obtained from {FULL_CONE_PRIVATE_NODE}: {vm_size:?}");
214            let image_id = get_value_for_resource(&resources, FULL_CONE_PRIVATE_NODE, IMAGE)?;
215            debug!("Node image id obtained from {FULL_CONE_PRIVATE_NODE}: {image_id:?}");
216            (vm_size, image_id)
217        } else {
218            (None, None)
219        };
220
221        let evm_node_count = resource_count(EVM_NODE);
222        debug!("EVM node count: {evm_node_count}");
223        let (evm_node_vm_size, evm_node_image_id) = if evm_node_count > 0 {
224            let emv_node_vm_size = get_value_for_resource(&resources, EVM_NODE, SIZE)?;
225            debug!("EVM node size: {emv_node_vm_size:?}");
226            let evm_node_image_id = get_value_for_resource(&resources, EVM_NODE, IMAGE)?;
227            debug!("EVM node image id: {evm_node_image_id:?}");
228            (emv_node_vm_size, evm_node_image_id)
229        } else {
230            (None, None)
231        };
232
233        let options = Self {
234            enable_build_vm,
235            evm_node_count: Some(evm_node_count),
236            evm_node_vm_size,
237            evm_node_image_id,
238            full_cone_nat_gateway_vm_size,
239            full_cone_private_node_vm_count: Some(full_cone_private_node_vm_count),
240            full_cone_private_node_volume_size,
241            genesis_vm_count: Some(genesis_node_vm_count),
242            genesis_node_volume_size,
243            name: name.to_string(),
244            nat_gateway_image_id,
245            node_image_id,
246            node_vm_count: Some(node_vm_count),
247            node_vm_size,
248            node_volume_size,
249            peer_cache_image_id,
250            peer_cache_node_vm_count: Some(peer_cache_node_vm_count),
251            peer_cache_node_vm_size,
252            peer_cache_node_volume_size,
253            symmetric_nat_gateway_vm_size,
254            symmetric_private_node_vm_count: Some(symmetric_private_node_vm_count),
255            symmetric_private_node_volume_size,
256            tfvars_filename: environment_details
257                .map(|details| details.environment_type.get_tfvars_filename(name)),
258            uploader_vm_count: Some(uploader_vm_count),
259            uploader_vm_size,
260            uploader_image_id,
261        };
262
263        Ok(options)
264    }
265}
266
267impl TestnetDeployer {
268    /// Create or update the infrastructure for a deployment.
269    pub fn create_or_update_infra(&self, options: &InfraRunOptions) -> Result<()> {
270        let start = Instant::now();
271        println!("Selecting {} workspace...", options.name);
272        self.terraform_runner.workspace_select(&options.name)?;
273
274        let args = build_terraform_args(options)?;
275
276        println!("Running terraform apply...");
277        self.terraform_runner
278            .apply(args, options.tfvars_filename.clone())?;
279        print_duration(start.elapsed());
280        Ok(())
281    }
282}
283
284#[derive(Clone, Debug)]
285pub struct UploaderInfraRunOptions {
286    pub enable_build_vm: bool,
287    pub name: String,
288    pub tfvars_filename: String,
289    pub uploader_vm_count: Option<u16>,
290    pub uploader_vm_size: Option<String>,
291    /// Set to None for new deployments, as the value will be fetched from tfvars.
292    pub uploader_image_id: Option<String>,
293}
294
295impl UploaderInfraRunOptions {
296    /// Generate the options for an existing uploader deployment.
297    pub async fn generate_existing(
298        name: &str,
299        terraform_runner: &TerraformRunner,
300        environment_details: &EnvironmentDetails,
301    ) -> Result<Self> {
302        let resources = terraform_runner.show(name)?;
303
304        let resource_count = |resource_name: &str| -> u16 {
305            resources
306                .iter()
307                .filter(|r| r.resource_name == resource_name)
308                .count() as u16
309        };
310
311        let uploader_vm_count = resource_count(UPLOADER);
312        debug!("Uploader count: {uploader_vm_count}");
313        let (uploader_vm_size, uploader_image_id) = if uploader_vm_count > 0 {
314            let vm_size = get_value_for_resource(&resources, UPLOADER, SIZE)?;
315            debug!("Uploader size: {vm_size:?}");
316            let image_id = get_value_for_resource(&resources, UPLOADER, IMAGE)?;
317            debug!("Uploader image id: {image_id:?}");
318            (vm_size, image_id)
319        } else {
320            (None, None)
321        };
322
323        let build_vm_count = resource_count(BUILD_VM);
324        debug!("Build VM count: {build_vm_count}");
325        let enable_build_vm = build_vm_count > 0;
326
327        let options = Self {
328            enable_build_vm,
329            name: name.to_string(),
330            tfvars_filename: environment_details
331                .environment_type
332                .get_tfvars_filename(name),
333            uploader_vm_count: Some(uploader_vm_count),
334            uploader_vm_size,
335            uploader_image_id,
336        };
337
338        Ok(options)
339    }
340
341    pub fn build_terraform_args(&self) -> Result<Vec<(String, String)>> {
342        let mut args = Vec::new();
343
344        args.push((
345            "use_custom_bin".to_string(),
346            self.enable_build_vm.to_string(),
347        ));
348
349        if let Some(uploader_vm_count) = self.uploader_vm_count {
350            args.push((
351                "uploader_vm_count".to_string(),
352                uploader_vm_count.to_string(),
353            ));
354        }
355        if let Some(uploader_vm_size) = &self.uploader_vm_size {
356            args.push((
357                "uploader_droplet_size".to_string(),
358                uploader_vm_size.clone(),
359            ));
360        }
361        if let Some(uploader_image_id) = &self.uploader_image_id {
362            args.push((
363                "uploader_droplet_image_id".to_string(),
364                uploader_image_id.clone(),
365            ));
366        }
367
368        Ok(args)
369    }
370}
371
372/// Build the terraform arguments from InfraRunOptions
373pub fn build_terraform_args(options: &InfraRunOptions) -> Result<Vec<(String, String)>> {
374    let mut args = Vec::new();
375
376    args.push((
377        "use_custom_bin".to_string(),
378        options.enable_build_vm.to_string(),
379    ));
380
381    if let Some(evm_node_count) = options.evm_node_count {
382        args.push(("evm_node_vm_count".to_string(), evm_node_count.to_string()));
383    }
384
385    if let Some(evm_node_vm_size) = &options.evm_node_vm_size {
386        args.push((
387            "evm_node_droplet_size".to_string(),
388            evm_node_vm_size.clone(),
389        ));
390    }
391
392    if let Some(emv_node_image_id) = &options.evm_node_image_id {
393        args.push((
394            "evm_node_droplet_image_id".to_string(),
395            emv_node_image_id.clone(),
396        ));
397    }
398
399    if let Some(full_cone_gateway_vm_size) = &options.full_cone_nat_gateway_vm_size {
400        args.push((
401            "full_cone_nat_gateway_droplet_size".to_string(),
402            full_cone_gateway_vm_size.clone(),
403        ));
404    }
405
406    if let Some(full_cone_private_node_vm_count) = options.full_cone_private_node_vm_count {
407        args.push((
408            "full_cone_private_node_vm_count".to_string(),
409            full_cone_private_node_vm_count.to_string(),
410        ));
411    }
412
413    if let Some(full_cone_private_node_volume_size) = options.full_cone_private_node_volume_size {
414        args.push((
415            "full_cone_private_node_volume_size".to_string(),
416            full_cone_private_node_volume_size.to_string(),
417        ));
418    }
419
420    if let Some(genesis_vm_count) = options.genesis_vm_count {
421        args.push(("genesis_vm_count".to_string(), genesis_vm_count.to_string()));
422    }
423
424    if let Some(genesis_node_volume_size) = options.genesis_node_volume_size {
425        args.push((
426            "genesis_node_volume_size".to_string(),
427            genesis_node_volume_size.to_string(),
428        ));
429    }
430
431    if let Some(nat_gateway_image_id) = &options.nat_gateway_image_id {
432        args.push((
433            "nat_gateway_droplet_image_id".to_string(),
434            nat_gateway_image_id.clone(),
435        ));
436    }
437
438    if let Some(node_image_id) = &options.node_image_id {
439        args.push(("node_droplet_image_id".to_string(), node_image_id.clone()));
440    }
441
442    if let Some(node_vm_count) = options.node_vm_count {
443        args.push(("node_vm_count".to_string(), node_vm_count.to_string()));
444    }
445
446    if let Some(node_vm_size) = &options.node_vm_size {
447        args.push(("node_droplet_size".to_string(), node_vm_size.clone()));
448    }
449
450    if let Some(node_volume_size) = options.node_volume_size {
451        args.push(("node_volume_size".to_string(), node_volume_size.to_string()));
452    }
453
454    if let Some(peer_cache_image_id) = &options.peer_cache_image_id {
455        args.push((
456            "peer_cache_droplet_image_id".to_string(),
457            peer_cache_image_id.clone(),
458        ));
459    }
460
461    if let Some(peer_cache_node_vm_count) = options.peer_cache_node_vm_count {
462        args.push((
463            "peer_cache_node_vm_count".to_string(),
464            peer_cache_node_vm_count.to_string(),
465        ));
466    }
467
468    if let Some(peer_cache_vm_size) = &options.peer_cache_node_vm_size {
469        args.push((
470            "peer_cache_droplet_size".to_string(),
471            peer_cache_vm_size.clone(),
472        ));
473    }
474
475    if let Some(reserved_ips) = crate::reserved_ip::get_reserved_ips_args(&options.name) {
476        args.push(("peer_cache_reserved_ips".to_string(), reserved_ips));
477    }
478
479    if let Some(peer_cache_node_volume_size) = options.peer_cache_node_volume_size {
480        args.push((
481            "peer_cache_node_volume_size".to_string(),
482            peer_cache_node_volume_size.to_string(),
483        ));
484    }
485
486    if let Some(nat_gateway_vm_size) = &options.symmetric_nat_gateway_vm_size {
487        args.push((
488            "symmetric_nat_gateway_droplet_size".to_string(),
489            nat_gateway_vm_size.clone(),
490        ));
491    }
492
493    if let Some(symmetric_private_node_vm_count) = options.symmetric_private_node_vm_count {
494        args.push((
495            "symmetric_private_node_vm_count".to_string(),
496            symmetric_private_node_vm_count.to_string(),
497        ));
498    }
499
500    if let Some(symmetric_private_node_volume_size) = options.symmetric_private_node_volume_size {
501        args.push((
502            "symmetric_private_node_volume_size".to_string(),
503            symmetric_private_node_volume_size.to_string(),
504        ));
505    }
506
507    if let Some(uploader_image_id) = &options.uploader_image_id {
508        args.push((
509            "uploader_droplet_image_id".to_string(),
510            uploader_image_id.clone(),
511        ));
512    }
513
514    if let Some(uploader_vm_count) = options.uploader_vm_count {
515        args.push((
516            "uploader_vm_count".to_string(),
517            uploader_vm_count.to_string(),
518        ));
519    }
520
521    if let Some(uploader_vm_size) = &options.uploader_vm_size {
522        args.push((
523            "uploader_droplet_size".to_string(),
524            uploader_vm_size.clone(),
525        ));
526    }
527
528    Ok(args)
529}
530
531/// Select a Terraform workspace for an environment.
532/// Returns an error if the environment doesn't exist.
533pub fn select_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
534    terraform_runner.init()?;
535    let workspaces = terraform_runner.workspace_list()?;
536    if !workspaces.contains(&name.to_string()) {
537        return Err(Error::EnvironmentDoesNotExist(name.to_string()));
538    }
539    terraform_runner.workspace_select(name)?;
540    println!("Selected {name} workspace");
541    Ok(())
542}
543
544pub fn delete_workspace(terraform_runner: &TerraformRunner, name: &str) -> Result<()> {
545    // The 'dev' workspace is one we always expect to exist, for admin purposes.
546    // You can't delete a workspace while it is selected, so we select 'dev' before we delete
547    // the current workspace.
548    terraform_runner.workspace_select("dev")?;
549    terraform_runner.workspace_delete(name)?;
550    println!("Deleted {name} workspace");
551    Ok(())
552}
553
554/// Extract a specific field value from terraform resources with proper type conversion.
555fn get_value_for_resource<T>(
556    resources: &[TerraformResource],
557    resource_name: &str,
558    field_name: &str,
559) -> Result<Option<T>, Error>
560where
561    T: From<TerraformValue>,
562{
563    let field_value = resources
564        .iter()
565        .filter(|r| r.resource_name == resource_name)
566        .try_fold(None, |acc_value: Option<serde_json::Value>, r| {
567            if let Some(value) = r.values.get(field_name) {
568                match acc_value {
569                    Some(ref existing_value) if existing_value != value => {
570                        log::error!("Expected value: {existing_value}, got value: {value}");
571                        Err(Error::TerraformResourceValueMismatch {
572                            expected: existing_value.to_string(),
573                            actual: value.to_string(),
574                        })
575                    }
576                    _ => Ok(Some(value.clone())),
577                }
578            } else {
579                Ok(acc_value)
580            }
581        })?;
582
583    Ok(field_value.map(TerraformValue::from).map(T::from))
584}
585
586/// Wrapper for terraform values to ensure proper conversion
587#[derive(Debug, Clone)]
588enum TerraformValue {
589    String(String),
590    Number(u64),
591    Bool(bool),
592    Other(serde_json::Value),
593}
594
595impl From<serde_json::Value> for TerraformValue {
596    fn from(value: serde_json::Value) -> Self {
597        if value.is_string() {
598            // Extract the inner string without quotes
599            // Unwrap is safe here because we checked is_string above
600            TerraformValue::String(value.as_str().unwrap().to_string())
601        } else if value.is_u64() {
602            // Unwrap is safe here because we checked is_u64 above
603            TerraformValue::Number(value.as_u64().unwrap())
604        } else if value.is_boolean() {
605            // Unwrap is safe here because we checked is_boolean above
606            TerraformValue::Bool(value.as_bool().unwrap())
607        } else {
608            TerraformValue::Other(value)
609        }
610    }
611}
612
613// Implement From<TerraformValue> for the types you need
614impl From<TerraformValue> for String {
615    fn from(value: TerraformValue) -> Self {
616        match value {
617            TerraformValue::String(s) => s,
618            TerraformValue::Number(n) => n.to_string(),
619            TerraformValue::Bool(b) => b.to_string(),
620            TerraformValue::Other(v) => v.to_string(),
621        }
622    }
623}
624
625impl From<TerraformValue> for u16 {
626    fn from(value: TerraformValue) -> Self {
627        match value {
628            TerraformValue::Number(n) => n as u16,
629            TerraformValue::String(s) => s.parse().unwrap_or(0),
630            _ => 0,
631        }
632    }
633}