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