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