oct_cloud/infra/
graph.rs

1use petgraph::{Incoming, Outgoing};
2
3use petgraph::visit::NodeIndexable;
4use std::collections::{HashMap, VecDeque};
5
6use petgraph::Graph;
7use petgraph::dot::Dot;
8use petgraph::graph::NodeIndex;
9
10use crate::aws::client;
11use crate::aws::types;
12use crate::infra::resource::{
13    DnsRecordManager, DnsRecordSpec, Ecr, EcrManager, EcrSpec, HostedZoneManager, HostedZoneSpec,
14    InboundRule, InstanceProfileManager, InstanceProfileSpec, InstanceRoleManager,
15    InstanceRoleSpec, InternetGatewayManager, InternetGatewaySpec, Manager, Node, ResourceSpecType,
16    ResourceType, RouteTableManager, RouteTableSpec, SecurityGroupManager, SecurityGroupSpec,
17    SpecNode, SubnetManager, SubnetSpec, Vm, VmManager, VmSpec, VpcManager, VpcSpec,
18};
19
20pub struct GraphManager {
21    ec2: client::Ec2,
22    iam: client::IAM,
23    ecr: client::ECR,
24    route53: client::Route53,
25}
26
27impl GraphManager {
28    pub async fn new() -> Self {
29        let region_provider = aws_sdk_ec2::config::Region::new("us-west-2");
30        let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
31            .region(region_provider)
32            .load()
33            .await;
34
35        let ec2_client = client::Ec2::new(aws_sdk_ec2::Client::new(&config));
36        let iam_client = client::IAM::new(aws_sdk_iam::Client::new(&config));
37        let ecr_client = client::ECR::new(aws_sdk_ecr::Client::new(&config));
38        let route53_client = client::Route53::new(aws_sdk_route53::Client::new(&config));
39
40        Self {
41            ec2: ec2_client,
42            iam: iam_client,
43            ecr: ecr_client,
44            route53: route53_client,
45        }
46    }
47
48    #[cfg(test)]
49    pub fn new_with_clients(
50        ec2_client: client::Ec2,
51        iam_client: client::IAM,
52        ecr_client: client::ECR,
53        route53_client: client::Route53,
54    ) -> Self {
55        Self {
56            ec2: ec2_client,
57            iam: iam_client,
58            ecr: ecr_client,
59            route53: route53_client,
60        }
61    }
62
63    /// Generates spec graph for the Genesis step
64    ///
65    /// Contains only the minimal required infra components to deploy the Leader node
66    pub fn get_genesis_graph(instance_type: types::InstanceType) -> Graph<SpecNode, String> {
67        let mut deps = Graph::<SpecNode, String>::new();
68        let root = deps.add_node(SpecNode::Root);
69
70        let vpc_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::Vpc(VpcSpec {
71            region: String::from("us-west-2"),
72            cidr_block: String::from("10.0.0.0/16"),
73            name: String::from("vpc-1"),
74        })));
75
76        let igw_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::InternetGateway(
77            InternetGatewaySpec,
78        )));
79
80        let route_table_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::RouteTable(
81            RouteTableSpec,
82        )));
83
84        let subnet_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::Subnet(SubnetSpec {
85            name: String::from("vpc-1-subnet"),
86            cidr_block: String::from("10.0.1.0/24"),
87            availability_zone: String::from("us-west-2a"),
88        })));
89
90        let security_group_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::SecurityGroup(
91            SecurityGroupSpec {
92                name: String::from("vpc-1-security-group"),
93                inbound_rules: vec![
94                    InboundRule {
95                        cidr_block: "0.0.0.0/0".to_string(),
96                        protocol: "tcp".to_string(),
97                        port: 80,
98                    },
99                    InboundRule {
100                        cidr_block: "0.0.0.0/0".to_string(),
101                        protocol: "tcp".to_string(),
102                        port: 31888,
103                    },
104                    InboundRule {
105                        cidr_block: "0.0.0.0/0".to_string(),
106                        protocol: "tcp".to_string(),
107                        port: 22,
108                    },
109                ],
110            },
111        )));
112
113        let instance_role_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::InstanceRole(
114            InstanceRoleSpec {
115                name: String::from("instance-role-1"),
116                assume_role_policy: String::from(
117                    r#"{ 
118                        "Version": "2012-10-17",
119                        "Statement": [
120                            {
121                                "Effect": "Allow",
122                                "Principal": {
123                                    "Service": "ec2.amazonaws.com"
124                                },
125                                "Action": "sts:AssumeRole"
126                            }
127                        ]
128                    }"#,
129                ),
130                policy_arns: vec![
131                    // TODO: Give more permissions to manage AWS infra
132                    String::from("arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"),
133                    String::from("arn:aws:iam::aws:policy/AmazonVPCFullAccess"),
134                ],
135            },
136        )));
137
138        let instance_profile_1 = deps.add_node(SpecNode::Resource(
139            ResourceSpecType::InstanceProfile(InstanceProfileSpec {
140                name: String::from("instance_profile_1"),
141            }),
142        ));
143
144        let user_data = String::from(
145            r#"#!/bin/bash
146        set -e
147        sudo apt update
148        sudo apt -y install podman
149        sudo systemctl start podman
150        sudo snap install aws-cli --classic
151
152        curl \
153            --output /home/ubuntu/oct-ctl \
154            -L \
155            https://github.com/opencloudtool/opencloudtool/releases/download/tip/oct-ctl \
156            && sudo chmod +x /home/ubuntu/oct-ctl \
157            && /home/ubuntu/oct-ctl & 
158        "#,
159        );
160
161        let vm = deps.add_node(SpecNode::Resource(ResourceSpecType::Vm(VmSpec {
162            instance_type,
163            ami: String::from("ami-04dd23e62ed049936"),
164            user_data,
165        })));
166
167        let edges = vec![
168            (root, instance_role_1, String::new()),
169            (root, vpc_1, String::new()),
170            (vpc_1, security_group_1, String::new()),
171            (vpc_1, subnet_1, String::new()),
172            (vpc_1, route_table_1, String::new()),
173            (vpc_1, igw_1, String::new()),
174            (igw_1, route_table_1, String::new()),
175            (route_table_1, subnet_1, String::new()),
176            (instance_role_1, instance_profile_1, String::new()),
177            (subnet_1, vm, String::new()),
178            (instance_profile_1, vm, String::new()),
179            (security_group_1, vm, String::new()),
180        ];
181
182        deps.extend_with_edges(&edges);
183
184        deps
185    }
186
187    /// Deploys Genesis graph
188    ///
189    /// Mostly duplicates logic of `deploy`, but without ECR creation
190    pub async fn deploy_genesis_graph(
191        &self,
192        graph: &Graph<SpecNode, String>,
193    ) -> Result<(Graph<Node, String>, Option<Vm>), Box<dyn std::error::Error>> {
194        let mut resource_graph = Graph::<Node, String>::new();
195        let mut edges = vec![];
196
197        let mut parents: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
198
199        let mut vm: Option<Vm> = None;
200
201        let result = kahn_traverse(graph)?;
202
203        for node_index in &result {
204            let parent_node_indexes = match parents.get(node_index) {
205                Some(parent_node_indexes) => parent_node_indexes.clone(),
206                None => Vec::new(),
207            };
208            let parent_nodes = parent_node_indexes
209                .iter()
210                .filter_map(|x| resource_graph.node_weight(*x))
211                .collect();
212
213            let node_to_deploy = &graph[*node_index];
214            let deployed_node = match node_to_deploy {
215                SpecNode::Root => Ok(Node::Root),
216                SpecNode::Resource(resource_type) => match resource_type {
217                    ResourceSpecType::HostedZone(resource) => {
218                        let manager = HostedZoneManager {
219                            client: &self.route53,
220                        };
221                        let output_resource = manager.create(resource, parent_nodes).await;
222
223                        match output_resource {
224                            Ok(output_resource) => {
225                                Ok(Node::Resource(ResourceType::HostedZone(output_resource)))
226                            }
227                            Err(e) => Err(Box::new(e)),
228                        }
229                    }
230                    ResourceSpecType::DnsRecord(resource) => {
231                        let manager = DnsRecordManager {
232                            client: &self.route53,
233                        };
234                        let output_resource = manager.create(resource, parent_nodes).await;
235
236                        match output_resource {
237                            Ok(output_resource) => {
238                                Ok(Node::Resource(ResourceType::DnsRecord(output_resource)))
239                            }
240                            Err(e) => Err(Box::new(e)),
241                        }
242                    }
243                    ResourceSpecType::Vpc(resource) => {
244                        let manager = VpcManager { client: &self.ec2 };
245                        let output_vpc = manager.create(resource, parent_nodes).await;
246
247                        match output_vpc {
248                            Ok(output_vpc) => Ok(Node::Resource(ResourceType::Vpc(output_vpc))),
249                            Err(e) => Err(Box::new(e)),
250                        }
251                    }
252                    ResourceSpecType::InternetGateway(resource) => {
253                        let manager = InternetGatewayManager { client: &self.ec2 };
254                        let output_igw = manager.create(resource, parent_nodes).await;
255
256                        match output_igw {
257                            Ok(output_igw) => {
258                                Ok(Node::Resource(ResourceType::InternetGateway(output_igw)))
259                            }
260                            Err(e) => Err(Box::new(e)),
261                        }
262                    }
263                    ResourceSpecType::RouteTable(resource) => {
264                        let manager = RouteTableManager { client: &self.ec2 };
265                        let output_route_table = manager.create(resource, parent_nodes).await;
266
267                        match output_route_table {
268                            Ok(output_route_table) => {
269                                Ok(Node::Resource(ResourceType::RouteTable(output_route_table)))
270                            }
271                            Err(e) => Err(Box::new(e)),
272                        }
273                    }
274                    ResourceSpecType::Subnet(resource) => {
275                        let manager = SubnetManager { client: &self.ec2 };
276                        let output_subnet = manager.create(resource, parent_nodes).await;
277
278                        match output_subnet {
279                            Ok(output_subnet) => {
280                                Ok(Node::Resource(ResourceType::Subnet(output_subnet)))
281                            }
282                            Err(e) => Err(Box::new(e)),
283                        }
284                    }
285                    ResourceSpecType::SecurityGroup(resource) => {
286                        let manager = SecurityGroupManager { client: &self.ec2 };
287                        let output_security_group = manager.create(resource, parent_nodes).await;
288
289                        match output_security_group {
290                            Ok(output_security_group) => Ok(Node::Resource(
291                                ResourceType::SecurityGroup(output_security_group),
292                            )),
293                            Err(e) => Err(Box::new(e)),
294                        }
295                    }
296                    ResourceSpecType::InstanceRole(resource) => {
297                        let manager = InstanceRoleManager { client: &self.iam };
298                        let output_instance_role = manager.create(resource, parent_nodes).await;
299
300                        match output_instance_role {
301                            Ok(output_instance_role) => Ok(Node::Resource(
302                                ResourceType::InstanceRole(output_instance_role),
303                            )),
304                            Err(e) => Err(Box::new(e)),
305                        }
306                    }
307                    ResourceSpecType::InstanceProfile(resource) => {
308                        let manager = InstanceProfileManager { client: &self.iam };
309                        let output_resource = manager.create(resource, parent_nodes).await;
310
311                        match output_resource {
312                            Ok(output_resource) => Ok(Node::Resource(
313                                ResourceType::InstanceProfile(output_resource),
314                            )),
315                            Err(e) => Err(Box::new(e)),
316                        }
317                    }
318                    ResourceSpecType::Ecr(resource) => {
319                        let manager = EcrManager { client: &self.ecr };
320                        let output_resource = manager.create(resource, parent_nodes).await;
321
322                        match output_resource {
323                            Ok(output_resource) => {
324                                Ok(Node::Resource(ResourceType::Ecr(output_resource)))
325                            }
326                            Err(e) => Err(Box::new(e)),
327                        }
328                    }
329                    ResourceSpecType::Vm(resource) => {
330                        let manager = VmManager { client: &self.ec2 };
331                        let output_vm = manager.create(resource, parent_nodes).await;
332
333                        match output_vm {
334                            Ok(output_vm) => {
335                                vm = Some(output_vm.clone());
336
337                                Ok(Node::Resource(ResourceType::Vm(output_vm)))
338                            }
339                            Err(e) => Err(Box::new(e)),
340                        }
341                    }
342                },
343            };
344
345            let Ok(deployed_node) = deployed_node else {
346                log::error!("Failed to create a resource {node_to_deploy:?} {deployed_node:?}");
347
348                break;
349            };
350
351            let created_resource_node_index = resource_graph.add_node(deployed_node.clone());
352
353            for parent_node_index in parent_node_indexes {
354                edges.push((
355                    parent_node_index,
356                    created_resource_node_index,
357                    String::new(),
358                ));
359            }
360
361            for neighbor_index in graph.neighbors(*node_index) {
362                parents
363                    .entry(neighbor_index)
364                    .or_insert_with(Vec::new)
365                    .push(created_resource_node_index);
366            }
367        }
368
369        resource_graph.extend_with_edges(&edges);
370
371        log::info!("Created graph {}", Dot::new(&resource_graph));
372
373        Ok((resource_graph, vm))
374    }
375
376    pub fn get_spec_graph(
377        instance_type: &types::InstanceType,
378        domain_name: Option<String>,
379    ) -> Graph<SpecNode, String> {
380        let mut deps = Graph::<SpecNode, String>::new();
381        let root = deps.add_node(SpecNode::Root);
382
383        let vpc_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::Vpc(VpcSpec {
384            region: String::from("us-west-2"),
385            cidr_block: String::from("10.0.0.0/16"),
386            name: String::from("vpc-1"),
387        })));
388
389        let igw_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::InternetGateway(
390            InternetGatewaySpec,
391        )));
392
393        let route_table_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::RouteTable(
394            RouteTableSpec,
395        )));
396
397        let subnet_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::Subnet(SubnetSpec {
398            name: String::from("vpc-1-subnet"),
399            cidr_block: String::from("10.0.1.0/24"),
400            availability_zone: String::from("us-west-2a"),
401        })));
402
403        let security_group_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::SecurityGroup(
404            SecurityGroupSpec {
405                name: String::from("vpc-1-security-group"),
406                inbound_rules: vec![
407                    InboundRule {
408                        cidr_block: "0.0.0.0/0".to_string(),
409                        protocol: "tcp".to_string(),
410                        port: 80,
411                    },
412                    InboundRule {
413                        cidr_block: "0.0.0.0/0".to_string(),
414                        protocol: "tcp".to_string(),
415                        port: 31888,
416                    },
417                    InboundRule {
418                        cidr_block: "0.0.0.0/0".to_string(),
419                        protocol: "tcp".to_string(),
420                        port: 22,
421                    },
422                ],
423            },
424        )));
425
426        let instance_role_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::InstanceRole(
427            InstanceRoleSpec {
428                name: String::from("instance-role-1"),
429                assume_role_policy: String::from(
430                    r#"{ 
431                        "Version": "2012-10-17",
432                        "Statement": [
433                            {
434                                "Effect": "Allow",
435                                "Principal": {
436                                    "Service": "ec2.amazonaws.com"
437                                },
438                                "Action": "sts:AssumeRole"
439                            }
440                        ]
441                    }"#,
442                ),
443                policy_arns: vec![String::from(
444                    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
445                )],
446            },
447        )));
448
449        let instance_profile_1 = deps.add_node(SpecNode::Resource(
450            ResourceSpecType::InstanceProfile(InstanceProfileSpec {
451                name: String::from("instance_profile_1"),
452            }),
453        ));
454
455        let ecr_1 = deps.add_node(SpecNode::Resource(ResourceSpecType::Ecr(EcrSpec {
456            name: String::from("ecr_1"),
457        })));
458
459        let user_data = String::from(
460            r#"#!/bin/bash
461        set -e
462        sudo apt update
463        sudo apt -y install podman
464        sudo systemctl start podman
465        sudo snap install aws-cli --classic
466
467        curl \
468            --output /home/ubuntu/oct-ctl \
469            -L \
470            https://github.com/opencloudtool/opencloudtool/releases/download/tip/oct-ctl \
471            && sudo chmod +x /home/ubuntu/oct-ctl \
472            && /home/ubuntu/oct-ctl & 
473        "#,
474        );
475
476        let vm = deps.add_node(SpecNode::Resource(ResourceSpecType::Vm(VmSpec {
477            instance_type: *instance_type,
478            ami: String::from("ami-04dd23e62ed049936"),
479            user_data,
480        })));
481
482        let mut edges = vec![
483            (root, ecr_1, String::new()),
484            (root, instance_role_1, String::new()),
485            (root, vpc_1, String::new()),
486            (vpc_1, security_group_1, String::new()),
487            (vpc_1, subnet_1, String::new()),
488            (vpc_1, route_table_1, String::new()),
489            (vpc_1, igw_1, String::new()),
490            (igw_1, route_table_1, String::new()),
491            (route_table_1, subnet_1, String::new()),
492            (instance_role_1, instance_profile_1, String::new()),
493            (subnet_1, vm, String::new()),
494            (instance_profile_1, vm, String::new()),
495            (security_group_1, vm, String::new()),
496            (ecr_1, vm, String::new()),
497        ];
498
499        if let Some(domain_name) = domain_name {
500            let hosted_zone = deps.add_node(SpecNode::Resource(ResourceSpecType::HostedZone(
501                HostedZoneSpec {
502                    region: String::from("us-west-2"),
503                    name: domain_name,
504                },
505            )));
506
507            // Insert at the first place to deploy it after all other root's children
508            edges.insert(0, (root, hosted_zone, String::new()));
509
510            let dns_record = deps.add_node(SpecNode::Resource(ResourceSpecType::DnsRecord(
511                DnsRecordSpec {
512                    record_type: types::RecordType::A,
513                    ttl: Some(3600),
514                },
515            )));
516
517            edges.push((vm, dns_record, String::new()));
518            edges.push((hosted_zone, dns_record, String::new()));
519        }
520
521        deps.extend_with_edges(&edges);
522
523        deps
524    }
525
526    /// Deploy spec graph
527    ///
528    /// Temporarily also returns a list of VMs and optional ECR
529    /// to be used for user services deployment
530    pub async fn deploy_spec_graph(
531        &self,
532        graph: &Graph<SpecNode, String>,
533    ) -> Result<(Graph<Node, String>, Option<Vm>, Option<Ecr>), Box<dyn std::error::Error>> {
534        let mut resource_graph = Graph::<Node, String>::new();
535        let mut edges = vec![];
536
537        let mut parents: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
538
539        let mut ecr: Option<Ecr> = None;
540        let mut vm: Option<Vm> = None;
541
542        let result = kahn_traverse(graph)?;
543
544        for node_index in &result {
545            let parent_node_indexes = match parents.get(node_index) {
546                Some(parent_node_indexes) => parent_node_indexes.clone(),
547                None => Vec::new(),
548            };
549            let parent_nodes = parent_node_indexes
550                .iter()
551                .filter_map(|x| resource_graph.node_weight(*x))
552                .collect();
553
554            let node_to_deploy = &graph[*node_index];
555            let deployed_node = match node_to_deploy {
556                SpecNode::Root => Ok(Node::Root),
557                SpecNode::Resource(resource_type) => match resource_type {
558                    ResourceSpecType::HostedZone(resource) => {
559                        let manager = HostedZoneManager {
560                            client: &self.route53,
561                        };
562                        let output_resource = manager.create(resource, parent_nodes).await;
563
564                        match output_resource {
565                            Ok(output_resource) => {
566                                Ok(Node::Resource(ResourceType::HostedZone(output_resource)))
567                            }
568                            Err(e) => Err(Box::new(e)),
569                        }
570                    }
571                    ResourceSpecType::DnsRecord(resource) => {
572                        let manager = DnsRecordManager {
573                            client: &self.route53,
574                        };
575                        let output_resource = manager.create(resource, parent_nodes).await;
576
577                        match output_resource {
578                            Ok(output_resource) => {
579                                Ok(Node::Resource(ResourceType::DnsRecord(output_resource)))
580                            }
581                            Err(e) => Err(Box::new(e)),
582                        }
583                    }
584                    ResourceSpecType::Vpc(resource) => {
585                        let manager = VpcManager { client: &self.ec2 };
586                        let output_vpc = manager.create(resource, parent_nodes).await;
587
588                        match output_vpc {
589                            Ok(output_vpc) => Ok(Node::Resource(ResourceType::Vpc(output_vpc))),
590                            Err(e) => Err(Box::new(e)),
591                        }
592                    }
593                    ResourceSpecType::InternetGateway(resource) => {
594                        let manager = InternetGatewayManager { client: &self.ec2 };
595                        let output_igw = manager.create(resource, parent_nodes).await;
596
597                        match output_igw {
598                            Ok(output_igw) => {
599                                Ok(Node::Resource(ResourceType::InternetGateway(output_igw)))
600                            }
601                            Err(e) => Err(Box::new(e)),
602                        }
603                    }
604                    ResourceSpecType::RouteTable(resource) => {
605                        let manager = RouteTableManager { client: &self.ec2 };
606                        let output_route_table = manager.create(resource, parent_nodes).await;
607
608                        match output_route_table {
609                            Ok(output_route_table) => {
610                                Ok(Node::Resource(ResourceType::RouteTable(output_route_table)))
611                            }
612                            Err(e) => Err(Box::new(e)),
613                        }
614                    }
615                    ResourceSpecType::Subnet(resource) => {
616                        let manager = SubnetManager { client: &self.ec2 };
617                        let output_subnet = manager.create(resource, parent_nodes).await;
618
619                        match output_subnet {
620                            Ok(output_subnet) => {
621                                Ok(Node::Resource(ResourceType::Subnet(output_subnet)))
622                            }
623                            Err(e) => Err(Box::new(e)),
624                        }
625                    }
626                    ResourceSpecType::SecurityGroup(resource) => {
627                        let manager = SecurityGroupManager { client: &self.ec2 };
628                        let output_security_group = manager.create(resource, parent_nodes).await;
629
630                        match output_security_group {
631                            Ok(output_security_group) => Ok(Node::Resource(
632                                ResourceType::SecurityGroup(output_security_group),
633                            )),
634                            Err(e) => Err(Box::new(e)),
635                        }
636                    }
637                    ResourceSpecType::InstanceRole(resource) => {
638                        let manager = InstanceRoleManager { client: &self.iam };
639                        let output_instance_role = manager.create(resource, parent_nodes).await;
640
641                        match output_instance_role {
642                            Ok(output_instance_role) => Ok(Node::Resource(
643                                ResourceType::InstanceRole(output_instance_role),
644                            )),
645                            Err(e) => Err(Box::new(e)),
646                        }
647                    }
648                    ResourceSpecType::InstanceProfile(resource) => {
649                        let manager = InstanceProfileManager { client: &self.iam };
650                        let output_resource = manager.create(resource, parent_nodes).await;
651
652                        match output_resource {
653                            Ok(output_resource) => Ok(Node::Resource(
654                                ResourceType::InstanceProfile(output_resource),
655                            )),
656                            Err(e) => Err(Box::new(e)),
657                        }
658                    }
659                    ResourceSpecType::Ecr(resource) => {
660                        let manager = EcrManager { client: &self.ecr };
661                        let output_resource = manager.create(resource, parent_nodes).await;
662
663                        match output_resource {
664                            Ok(output_resource) => {
665                                ecr = Some(output_resource.clone());
666
667                                Ok(Node::Resource(ResourceType::Ecr(output_resource)))
668                            }
669                            Err(e) => Err(Box::new(e)),
670                        }
671                    }
672                    ResourceSpecType::Vm(resource) => {
673                        let manager = VmManager { client: &self.ec2 };
674                        let output_vm = manager.create(resource, parent_nodes).await;
675
676                        match output_vm {
677                            Ok(output_vm) => {
678                                vm = Some(output_vm.clone());
679
680                                Ok(Node::Resource(ResourceType::Vm(output_vm)))
681                            }
682                            Err(e) => Err(Box::new(e)),
683                        }
684                    }
685                },
686            };
687
688            let Ok(created_node) = deployed_node else {
689                log::error!("Failed to create a resource {node_to_deploy:?}");
690
691                break;
692            };
693
694            let created_resource_node_index = resource_graph.add_node(created_node.clone());
695
696            for parent_node_index in parent_node_indexes {
697                edges.push((
698                    parent_node_index,
699                    created_resource_node_index,
700                    String::new(),
701                ));
702            }
703
704            for neighbor_index in graph.neighbors(*node_index) {
705                parents
706                    .entry(neighbor_index)
707                    .or_insert_with(Vec::new)
708                    .push(created_resource_node_index);
709            }
710        }
711
712        resource_graph.extend_with_edges(&edges);
713
714        log::info!("Created graph {}", Dot::new(&resource_graph));
715
716        Ok((resource_graph, vm, ecr))
717    }
718
719    /// Deploy arbitrary graph
720    pub async fn deploy(
721        &self,
722        graph: &Graph<SpecNode, String>,
723    ) -> Result<Graph<Node, String>, Box<dyn std::error::Error>> {
724        let mut resource_graph = Graph::<Node, String>::new();
725        let mut edges = vec![];
726
727        let mut parents: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
728
729        let result = kahn_traverse(graph)?;
730
731        for node_index in &result {
732            let parent_node_indexes = match parents.get(node_index) {
733                Some(parent_node_indexes) => parent_node_indexes.clone(),
734                None => Vec::new(),
735            };
736            let parent_nodes = parent_node_indexes
737                .iter()
738                .filter_map(|x| resource_graph.node_weight(*x))
739                .collect();
740
741            let node_to_deploy = &graph[*node_index];
742            let deployed_node = match node_to_deploy {
743                SpecNode::Root => Ok(Node::Root),
744                SpecNode::Resource(resource_type) => match resource_type {
745                    ResourceSpecType::HostedZone(resource) => {
746                        let manager = HostedZoneManager {
747                            client: &self.route53,
748                        };
749                        let output_resource = manager.create(resource, parent_nodes).await;
750
751                        match output_resource {
752                            Ok(output_resource) => {
753                                Ok(Node::Resource(ResourceType::HostedZone(output_resource)))
754                            }
755                            Err(e) => Err(Box::new(e)),
756                        }
757                    }
758                    ResourceSpecType::DnsRecord(resource) => {
759                        let manager = DnsRecordManager {
760                            client: &self.route53,
761                        };
762                        let output_resource = manager.create(resource, parent_nodes).await;
763
764                        match output_resource {
765                            Ok(output_resource) => {
766                                Ok(Node::Resource(ResourceType::DnsRecord(output_resource)))
767                            }
768                            Err(e) => Err(Box::new(e)),
769                        }
770                    }
771                    ResourceSpecType::Vpc(resource) => {
772                        let manager = VpcManager { client: &self.ec2 };
773                        let output_vpc = manager.create(resource, parent_nodes).await;
774
775                        match output_vpc {
776                            Ok(output_vpc) => Ok(Node::Resource(ResourceType::Vpc(output_vpc))),
777                            Err(e) => Err(Box::new(e)),
778                        }
779                    }
780                    ResourceSpecType::InternetGateway(resource) => {
781                        let manager = InternetGatewayManager { client: &self.ec2 };
782                        let output_igw = manager.create(resource, parent_nodes).await;
783
784                        match output_igw {
785                            Ok(output_igw) => {
786                                Ok(Node::Resource(ResourceType::InternetGateway(output_igw)))
787                            }
788                            Err(e) => Err(Box::new(e)),
789                        }
790                    }
791                    ResourceSpecType::RouteTable(resource) => {
792                        let manager = RouteTableManager { client: &self.ec2 };
793                        let output_route_table = manager.create(resource, parent_nodes).await;
794
795                        match output_route_table {
796                            Ok(output_route_table) => {
797                                Ok(Node::Resource(ResourceType::RouteTable(output_route_table)))
798                            }
799                            Err(e) => Err(Box::new(e)),
800                        }
801                    }
802                    ResourceSpecType::Subnet(resource) => {
803                        let manager = SubnetManager { client: &self.ec2 };
804                        let output_subnet = manager.create(resource, parent_nodes).await;
805
806                        match output_subnet {
807                            Ok(output_subnet) => {
808                                Ok(Node::Resource(ResourceType::Subnet(output_subnet)))
809                            }
810                            Err(e) => Err(Box::new(e)),
811                        }
812                    }
813                    ResourceSpecType::SecurityGroup(resource) => {
814                        let manager = SecurityGroupManager { client: &self.ec2 };
815                        let output_security_group = manager.create(resource, parent_nodes).await;
816
817                        match output_security_group {
818                            Ok(output_security_group) => Ok(Node::Resource(
819                                ResourceType::SecurityGroup(output_security_group),
820                            )),
821                            Err(e) => Err(Box::new(e)),
822                        }
823                    }
824                    ResourceSpecType::InstanceRole(resource) => {
825                        let manager = InstanceRoleManager { client: &self.iam };
826                        let output_instance_role = manager.create(resource, parent_nodes).await;
827
828                        match output_instance_role {
829                            Ok(output_instance_role) => Ok(Node::Resource(
830                                ResourceType::InstanceRole(output_instance_role),
831                            )),
832                            Err(e) => Err(Box::new(e)),
833                        }
834                    }
835                    ResourceSpecType::InstanceProfile(resource) => {
836                        let manager = InstanceProfileManager { client: &self.iam };
837                        let output_resource = manager.create(resource, parent_nodes).await;
838
839                        match output_resource {
840                            Ok(output_resource) => Ok(Node::Resource(
841                                ResourceType::InstanceProfile(output_resource),
842                            )),
843                            Err(e) => Err(Box::new(e)),
844                        }
845                    }
846                    ResourceSpecType::Ecr(resource) => {
847                        let manager = EcrManager { client: &self.ecr };
848                        let output_resource = manager.create(resource, parent_nodes).await;
849
850                        match output_resource {
851                            Ok(output_resource) => {
852                                Ok(Node::Resource(ResourceType::Ecr(output_resource)))
853                            }
854                            Err(e) => Err(Box::new(e)),
855                        }
856                    }
857                    ResourceSpecType::Vm(resource) => {
858                        let manager = VmManager { client: &self.ec2 };
859                        let output_vm = manager.create(resource, parent_nodes).await;
860
861                        match output_vm {
862                            Ok(output_vm) => Ok(Node::Resource(ResourceType::Vm(output_vm))),
863                            Err(e) => Err(Box::new(e)),
864                        }
865                    }
866                },
867            };
868
869            let Ok(created_node) = deployed_node else {
870                log::error!("Failed to create a resource {node_to_deploy:?}");
871
872                break;
873            };
874
875            let created_resource_node_index = resource_graph.add_node(created_node.clone());
876
877            for parent_node_index in parent_node_indexes {
878                edges.push((
879                    parent_node_index,
880                    created_resource_node_index,
881                    String::new(),
882                ));
883            }
884
885            for neighbor_index in graph.neighbors(*node_index) {
886                parents
887                    .entry(neighbor_index)
888                    .or_insert_with(Vec::new)
889                    .push(created_resource_node_index);
890            }
891        }
892
893        resource_graph.extend_with_edges(&edges);
894
895        log::info!("Created graph {}", Dot::new(&resource_graph));
896
897        Ok(resource_graph)
898    }
899
900    /// Destroys resource graph
901    ///
902    /// Modifies the input graph by deleting all the destroyed nodes.
903    /// In case of a resource destruction failure, returns Err and
904    /// the remaining resources in `graph` must be handled accordingly.
905    pub async fn destroy(
906        &self,
907        graph: &mut Graph<Node, String>,
908    ) -> Result<(), Box<dyn std::error::Error>> {
909        let mut parents: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
910
911        // Remove resources
912        let mut queue_to_traverse: VecDeque<NodeIndex> = VecDeque::new();
913        let root_index = graph.from_index(0);
914        for node_index in graph.neighbors(root_index) {
915            queue_to_traverse.push_back(node_index);
916
917            parents
918                .entry(node_index)
919                .or_insert_with(Vec::new)
920                .push(root_index);
921        }
922
923        // Prepare queue to destroy
924        while let Some(node_index) = queue_to_traverse.pop_front() {
925            if let Some(_elem) = graph.node_weight(node_index) {
926                for neighbor_index in graph.neighbors(node_index) {
927                    if !parents.contains_key(&neighbor_index) {
928                        queue_to_traverse.push_back(neighbor_index);
929                    }
930
931                    parents
932                        .entry(neighbor_index)
933                        .or_insert_with(Vec::new)
934                        .push(node_index);
935                }
936            }
937        }
938
939        let result = kahn_traverse(graph)?;
940
941        let mut destroyed_nodes: Vec<NodeIndex> = Vec::new();
942
943        // Destroying resources in reversed order
944        for node_index in result.iter().rev() {
945            let parent_node_indexes = match parents.get(node_index) {
946                Some(parent_node_indexes) => parent_node_indexes.clone(),
947                None => Vec::new(),
948            };
949            let parent_nodes = parent_node_indexes
950                .iter()
951                .filter_map(|x| graph.node_weight(*x))
952                .collect();
953
954            let node_to_destroy = &graph[*node_index];
955            let destroyed_node = match node_to_destroy {
956                Node::Root => Ok(()),
957                Node::Resource(resource_type) => match resource_type {
958                    ResourceType::HostedZone(resource) => {
959                        let manager = HostedZoneManager {
960                            client: &self.route53,
961                        };
962                        manager.destroy(resource, parent_nodes).await
963                    }
964                    ResourceType::DnsRecord(resource) => {
965                        let manager = DnsRecordManager {
966                            client: &self.route53,
967                        };
968                        manager.destroy(resource, parent_nodes).await
969                    }
970                    ResourceType::Vpc(resource) => {
971                        let manager = VpcManager { client: &self.ec2 };
972                        manager.destroy(resource, parent_nodes).await
973                    }
974                    ResourceType::InternetGateway(resource) => {
975                        let manager = InternetGatewayManager { client: &self.ec2 };
976                        manager.destroy(resource, parent_nodes).await
977                    }
978                    ResourceType::RouteTable(resource) => {
979                        let manager = RouteTableManager { client: &self.ec2 };
980                        manager.destroy(resource, parent_nodes).await
981                    }
982                    ResourceType::Subnet(resource) => {
983                        let manager = SubnetManager { client: &self.ec2 };
984                        manager.destroy(resource, parent_nodes).await
985                    }
986                    ResourceType::SecurityGroup(resource) => {
987                        let manager = SecurityGroupManager { client: &self.ec2 };
988                        manager.destroy(resource, parent_nodes).await
989                    }
990                    ResourceType::InstanceRole(resource) => {
991                        let manager = InstanceRoleManager { client: &self.iam };
992                        manager.destroy(resource, parent_nodes).await
993                    }
994                    ResourceType::InstanceProfile(resource) => {
995                        let manager = InstanceProfileManager { client: &self.iam };
996                        manager.destroy(resource, parent_nodes).await
997                    }
998                    ResourceType::Ecr(resource) => {
999                        let manager = EcrManager { client: &self.ecr };
1000                        manager.destroy(resource, parent_nodes).await
1001                    }
1002                    ResourceType::Vm(resource) => {
1003                        let manager = VmManager { client: &self.ec2 };
1004                        manager.destroy(resource, parent_nodes).await
1005                    }
1006                    ResourceType::None => Err("Unexpected case ResourceType::None".into()),
1007                },
1008            };
1009
1010            match destroyed_node {
1011                Ok(()) => {
1012                    log::info!("Destroyed {node_to_destroy:?}");
1013
1014                    destroyed_nodes.push(*node_index);
1015                }
1016                Err(e) => {
1017                    log::error!("Failed to destroy {node_to_destroy:?}: {e}");
1018
1019                    break;
1020                }
1021            }
1022        }
1023
1024        graph.retain_nodes(|_, node_idx| !destroyed_nodes.contains(&node_idx));
1025
1026        if graph.edge_count() == 0 {
1027            Ok(())
1028        } else {
1029            Err("Failed to destroy some resources".into())
1030        }
1031    }
1032}
1033
1034/// Kahn's Algorithm Implementation
1035pub fn kahn_traverse<T>(
1036    graph: &Graph<T, String>,
1037) -> Result<Vec<NodeIndex>, Box<dyn std::error::Error>> {
1038    // 1. Calculate the in-degree for each node.
1039    let mut in_degrees = vec![0; graph.node_bound()];
1040    for node in graph.node_indices() {
1041        in_degrees[graph.to_index(node)] = graph.neighbors_directed(node, Incoming).count();
1042    }
1043
1044    // 2. Initialize a queue with all nodes having an in-degree of 0.
1045    let mut queue: VecDeque<NodeIndex> = graph
1046        .node_indices()
1047        .filter(|&i| in_degrees[graph.to_index(i)] == 0)
1048        .collect();
1049
1050    let mut result = Vec::with_capacity(graph.node_count());
1051
1052    // 3. Process the queue.
1053    while let Some(node) = queue.pop_front() {
1054        result.push(node);
1055
1056        // For each neighbor of the processed node, decrement its in-degree.
1057        for neighbor in graph.neighbors_directed(node, Outgoing) {
1058            let neighbor_idx = graph.to_index(neighbor);
1059            in_degrees[neighbor_idx] -= 1;
1060
1061            // If a neighbor's in-degree becomes 0, add it to the queue.
1062            if in_degrees[neighbor_idx] == 0 {
1063                queue.push_back(neighbor);
1064            }
1065        }
1066    }
1067
1068    if result.len() < graph.node_count() {
1069        return Err("Cycle detected in graph".into());
1070    }
1071
1072    Ok(result)
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use super::*;
1078    use crate::aws::types::InstanceType;
1079    use crate::infra::resource::*;
1080    use crate::infra::resource::{ResourceSpecType, SpecNode};
1081    use mockall::predicate::eq;
1082
1083    #[test]
1084    fn test_get_spec_graph_with_one_instance_no_domain() {
1085        // Arrange
1086        let instance_type = InstanceType::T3Micro;
1087        let domain_name = None;
1088
1089        // Act
1090        let graph = GraphManager::get_spec_graph(&instance_type, domain_name);
1091
1092        // Assert
1093        assert_eq!(graph.node_count(), 10);
1094        assert_eq!(graph.edge_count(), 10 + 4);
1095
1096        let vm_nodes_count = graph
1097            .raw_nodes()
1098            .iter()
1099            .filter(|node| matches!(&node.weight, SpecNode::Resource(ResourceSpecType::Vm(_))))
1100            .count();
1101        assert_eq!(vm_nodes_count, 1);
1102    }
1103
1104    #[test]
1105    fn test_get_spec_graph_with_one_instance_and_domain() {
1106        // Arrange
1107        let instance_type = InstanceType::T3Micro;
1108        let domain_name = Some(String::from("example.com"));
1109
1110        // Act
1111        let graph = GraphManager::get_spec_graph(&instance_type, domain_name);
1112
1113        // Assert
1114        assert_eq!(graph.node_count(), 10 + 2);
1115        assert_eq!(graph.edge_count(), 11 + 6);
1116
1117        let vm_nodes_count = graph
1118            .raw_nodes()
1119            .iter()
1120            .filter(|node| matches!(&node.weight, SpecNode::Resource(ResourceSpecType::Vm(_))))
1121            .count();
1122        assert_eq!(vm_nodes_count, 1);
1123
1124        let hosted_zone_nodes_count = graph
1125            .raw_nodes()
1126            .iter()
1127            .filter(|node| {
1128                matches!(
1129                    &node.weight,
1130                    SpecNode::Resource(ResourceSpecType::HostedZone(_))
1131                )
1132            })
1133            .count();
1134        assert_eq!(hosted_zone_nodes_count, 1);
1135
1136        let dns_record_nodes_count = graph
1137            .raw_nodes()
1138            .iter()
1139            .filter(|node| {
1140                matches!(
1141                    &node.weight,
1142                    SpecNode::Resource(ResourceSpecType::DnsRecord(_))
1143                )
1144            })
1145            .count();
1146        assert_eq!(dns_record_nodes_count, 1);
1147    }
1148
1149    #[tokio::test]
1150    async fn test_deploy_spec_graph_with_one_instance_no_domain() {
1151        // Arrange
1152        let instance_type = InstanceType::T3Micro;
1153        let domain_name = None;
1154
1155        let spec_graph = GraphManager::get_spec_graph(&instance_type, domain_name);
1156
1157        let mut ec2_client_mock = client::Ec2::default();
1158        let mut iam_client_mock = client::IAM::default();
1159        let mut ecr_client_mock = client::ECR::default();
1160        let route53_client_mock = client::Route53::default();
1161
1162        // Expectations for resource creation
1163        ec2_client_mock
1164            .expect_create_vpc()
1165            .with(eq(String::from("10.0.0.0/16")), eq(String::from("vpc-1")))
1166            .return_once(|_, _| Ok(String::from("vpc-id-1")));
1167
1168        iam_client_mock
1169            .expect_create_instance_iam_role()
1170            .with(
1171                eq(String::from("instance-role-1")),
1172                eq(String::from(
1173                    r#"{ 
1174                        "Version": "2012-10-17",
1175                        "Statement": [
1176                            {
1177                                "Effect": "Allow",
1178                                "Principal": {
1179                                    "Service": "ec2.amazonaws.com"
1180                                },
1181                                "Action": "sts:AssumeRole"
1182                            }
1183                        ]
1184                    }"#,
1185                )),
1186                eq(vec![String::from(
1187                    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
1188                )]),
1189            )
1190            .return_once(|_, _, _| Ok(()));
1191
1192        ecr_client_mock
1193            .expect_create_repository()
1194            .with(eq(String::from("ecr_1")))
1195            .return_once(|_| Ok((String::from("ecr-id-1"), String::from("ecr-uri-1/foo"))));
1196
1197        ec2_client_mock
1198            .expect_create_internet_gateway()
1199            .with(eq(String::from("vpc-id-1")))
1200            .return_once(|_| Ok(String::from("igw-id-1")));
1201
1202        ec2_client_mock
1203            .expect_create_route_table()
1204            .with(eq(String::from("vpc-id-1")))
1205            .return_once(|_| Ok(String::from("rt-id-1")));
1206
1207        ec2_client_mock
1208            .expect_add_public_route()
1209            .with(eq(String::from("rt-id-1")), eq(String::from("igw-id-1")))
1210            .return_once(|_, _| Ok(()));
1211
1212        ec2_client_mock
1213            .expect_create_subnet()
1214            .with(
1215                eq(String::from("vpc-id-1")),
1216                eq(String::from("10.0.1.0/24")),
1217                eq(String::from("us-west-2a")),
1218                eq(String::from("vpc-1-subnet")),
1219            )
1220            .return_once(|_, _, _, _| Ok(String::from("subnet-id-1")));
1221
1222        ec2_client_mock
1223            .expect_enable_auto_assign_ip_addresses_for_subnet()
1224            .with(eq(String::from("subnet-id-1")))
1225            .return_once(|_| Ok(()));
1226
1227        ec2_client_mock
1228            .expect_associate_route_table_with_subnet()
1229            .with(eq(String::from("rt-id-1")), eq(String::from("subnet-id-1")))
1230            .return_once(|_, _| Ok(()));
1231
1232        ec2_client_mock
1233            .expect_create_security_group()
1234            .with(
1235                eq(String::from("vpc-id-1")),
1236                eq(String::from("vpc-1-security-group")),
1237                eq(String::from("No description")),
1238            )
1239            .return_once(|_, _, _| Ok(String::from("sg-id-1")));
1240
1241        ec2_client_mock
1242            .expect_allow_inbound_traffic_for_security_group()
1243            .with(
1244                eq(String::from("sg-id-1")),
1245                eq(String::from("tcp")),
1246                eq(80),
1247                eq(String::from("0.0.0.0/0")),
1248            )
1249            .return_once(|_, _, _, _| Ok(()));
1250        ec2_client_mock
1251            .expect_allow_inbound_traffic_for_security_group()
1252            .with(
1253                eq(String::from("sg-id-1")),
1254                eq(String::from("tcp")),
1255                eq(31888),
1256                eq(String::from("0.0.0.0/0")),
1257            )
1258            .return_once(|_, _, _, _| Ok(()));
1259        ec2_client_mock
1260            .expect_allow_inbound_traffic_for_security_group()
1261            .with(
1262                eq(String::from("sg-id-1")),
1263                eq(String::from("tcp")),
1264                eq(22),
1265                eq(String::from("0.0.0.0/0")),
1266            )
1267            .return_once(|_, _, _, _| Ok(()));
1268
1269        iam_client_mock
1270            .expect_create_instance_profile()
1271            .with(
1272                eq(String::from("instance_profile_1")),
1273                eq(vec![String::from("instance-role-1")]),
1274            )
1275            .return_once(|_, _| Ok(()));
1276
1277        ec2_client_mock
1278            .expect_run_instances()
1279            .return_once(|_, _, _, _, _, _| {
1280                let instance = aws_sdk_ec2::types::Instance::builder()
1281                    .instance_id("vm-id-1")
1282                    .build();
1283                Ok(
1284                    aws_sdk_ec2::operation::run_instances::RunInstancesOutput::builder()
1285                        .instances(instance)
1286                        .build(),
1287                )
1288            });
1289
1290        ec2_client_mock
1291            .expect_describe_instances()
1292            .with(eq(String::from("vm-id-1")))
1293            .return_once(|_| {
1294                Ok(aws_sdk_ec2::types::Instance::builder()
1295                    .public_ip_address("1.2.3.4")
1296                    .build())
1297            });
1298
1299        let graph_manager = GraphManager::new_with_clients(
1300            ec2_client_mock,
1301            iam_client_mock,
1302            ecr_client_mock,
1303            route53_client_mock,
1304        );
1305
1306        // Act
1307        let (resource_graph, vm, ecr) = graph_manager
1308            .deploy_spec_graph(&spec_graph)
1309            .await
1310            .expect("Failed to deploy");
1311
1312        // Assert
1313        assert_eq!(resource_graph.node_count(), 10); // root + 9 resources
1314        assert_eq!(resource_graph.edge_count(), 14);
1315
1316        assert_eq!(
1317            vm,
1318            Some(Vm {
1319                id: String::from("vm-id-1"),
1320                public_ip: String::from("1.2.3.4"),
1321                ami: String::from("ami-04dd23e62ed049936"),
1322                instance_type: InstanceType::T3Micro,
1323                user_data: String::from(
1324                    r#"#!/bin/bash
1325        set -e
1326        sudo apt update
1327        sudo apt -y install podman
1328        sudo systemctl start podman
1329        sudo snap install aws-cli --classic
1330
1331        curl \
1332            --output /home/ubuntu/oct-ctl \
1333            -L \
1334            https://github.com/opencloudtool/opencloudtool/releases/download/tip/oct-ctl \
1335            && sudo chmod +x /home/ubuntu/oct-ctl \
1336            && /home/ubuntu/oct-ctl & 
1337        "#
1338                )
1339            })
1340        );
1341
1342        assert_eq!(
1343            ecr.expect("Failed to get ECR"),
1344            Ecr {
1345                id: String::from("ecr-id-1"),
1346                name: String::from("ecr_1"),
1347                uri: String::from("ecr-uri-1/foo"),
1348            }
1349        );
1350    }
1351
1352    #[tokio::test]
1353    async fn test_deploy_spec_graph_empty_graph() {
1354        // Arrange
1355        let spec_graph = Graph::<SpecNode, String>::new();
1356
1357        let ec2_client_mock = client::Ec2::default();
1358        let iam_client_mock = client::IAM::default();
1359        let ecr_client_mock = client::ECR::default();
1360        let route53_client_mock = client::Route53::default();
1361
1362        let graph_manager = GraphManager::new_with_clients(
1363            ec2_client_mock,
1364            iam_client_mock,
1365            ecr_client_mock,
1366            route53_client_mock,
1367        );
1368
1369        // Act
1370        let (resource_graph, vm, ecr) = graph_manager
1371            .deploy_spec_graph(&spec_graph)
1372            .await
1373            .expect("Failed to deploy");
1374
1375        // Assert
1376        assert_eq!(resource_graph.node_count(), 0);
1377        assert_eq!(resource_graph.edge_count(), 0);
1378        assert!(vm.is_none());
1379        assert!(ecr.is_none());
1380    }
1381
1382    #[tokio::test]
1383    async fn test_deploy_spec_graph_resource_creation_fails() {
1384        // Arrange
1385        let mut spec_graph = Graph::<SpecNode, String>::new();
1386        let root = spec_graph.add_node(SpecNode::Root);
1387        let vpc_1 = spec_graph.add_node(SpecNode::Resource(ResourceSpecType::Vpc(VpcSpec {
1388            region: String::from("us-west-2"),
1389            cidr_block: String::from("10.0.0.0/16"),
1390            name: String::from("vpc-1"),
1391        })));
1392        let subnet_1 =
1393            spec_graph.add_node(SpecNode::Resource(ResourceSpecType::Subnet(SubnetSpec {
1394                name: String::from("vpc-1-subnet"),
1395                cidr_block: String::from("10.0.1.0/24"),
1396                availability_zone: String::from("us-west-2a"),
1397            })));
1398        let edges = vec![
1399            (root, vpc_1, String::new()),
1400            (vpc_1, subnet_1, String::new()),
1401        ];
1402        spec_graph.extend_with_edges(&edges);
1403
1404        let mut ec2_client_mock = client::Ec2::default();
1405        let iam_client_mock = client::IAM::default();
1406        let ecr_client_mock = client::ECR::default();
1407        let route53_client_mock = client::Route53::default();
1408
1409        ec2_client_mock
1410            .expect_create_vpc()
1411            .with(eq(String::from("10.0.0.0/16")), eq(String::from("vpc-1")))
1412            .return_once(|_, _| Ok(String::from("vpc-id-1")));
1413
1414        // Simulate Subnet creation failure
1415        ec2_client_mock
1416            .expect_create_subnet()
1417            .with(
1418                eq(String::from("vpc-id-1")),
1419                eq(String::from("10.0.1.0/24")),
1420                eq(String::from("us-west-2a")),
1421                eq(String::from("vpc-1-subnet")),
1422            )
1423            .return_once(|_, _, _, _| Err("Subnet creation failed".into()));
1424
1425        let graph_manager = GraphManager::new_with_clients(
1426            ec2_client_mock,
1427            iam_client_mock,
1428            ecr_client_mock,
1429            route53_client_mock,
1430        );
1431
1432        // Act
1433        let (resource_graph, vm, ecr) = graph_manager
1434            .deploy_spec_graph(&spec_graph)
1435            .await
1436            .expect("Failed to deploy");
1437
1438        // Assert
1439        // 1 root + VPC
1440        assert_eq!(resource_graph.node_count(), 2);
1441        assert_eq!(resource_graph.edge_count(), 1);
1442        assert!(vm.is_none());
1443        assert!(ecr.is_none());
1444
1445        let vpc_node_exists = resource_graph
1446            .node_weights()
1447            .any(|w| matches!(w, Node::Resource(ResourceType::Vpc(_))));
1448        assert!(vpc_node_exists);
1449        let subnet_node_exists = resource_graph
1450            .node_weights()
1451            .any(|w| matches!(w, Node::Resource(ResourceType::Subnet(_))));
1452        assert!(!subnet_node_exists);
1453    }
1454
1455    #[tokio::test]
1456    async fn test_destroy_with_one_instance_no_domain() {
1457        // Arrange
1458        let mut resource_graph = get_test_resource_graph();
1459
1460        let mut ec2_client_mock = client::Ec2::default();
1461        let mut iam_client_mock = client::IAM::default();
1462        let mut ecr_client_mock = client::ECR::default();
1463        let route53_client_mock = client::Route53::default();
1464
1465        // Expectations for resource destruction
1466        ec2_client_mock
1467            .expect_terminate_instance()
1468            .with(eq(String::from("vm-id-1")))
1469            .return_once(|_| Ok(()));
1470
1471        // VmManager::is_terminated mock
1472        ec2_client_mock
1473            .expect_describe_instances()
1474            .with(eq(String::from("vm-id-1")))
1475            .return_once(|_| {
1476                Ok(aws_sdk_ec2::types::Instance::builder()
1477                    .state(
1478                        aws_sdk_ec2::types::InstanceState::builder()
1479                            .name(aws_sdk_ec2::types::InstanceStateName::Terminated)
1480                            .build(),
1481                    )
1482                    .build())
1483            });
1484
1485        iam_client_mock
1486            .expect_delete_instance_profile()
1487            .with(
1488                eq(String::from("instance_profile_1")),
1489                eq(vec![String::from("instance-role-1")]),
1490            )
1491            .return_once(|_, _| Ok(()));
1492
1493        ec2_client_mock
1494            .expect_delete_security_group()
1495            .with(eq(String::from("sg-id-1")))
1496            .return_once(|_| Ok(()));
1497
1498        ec2_client_mock
1499            .expect_disassociate_route_table_with_subnet()
1500            .with(eq(String::from("rt-id-1")), eq(String::from("subnet-id-1")))
1501            .return_once(|_, _| Ok(()));
1502
1503        ec2_client_mock
1504            .expect_delete_subnet()
1505            .with(eq(String::from("subnet-id-1")))
1506            .return_once(|_| Ok(()));
1507
1508        ec2_client_mock
1509            .expect_delete_route_table()
1510            .with(eq(String::from("rt-id-1")))
1511            .return_once(|_| Ok(()));
1512
1513        ec2_client_mock
1514            .expect_delete_internet_gateway()
1515            .with(eq(String::from("igw-id-1")), eq(String::from("vpc-id-1")))
1516            .return_once(|_, _| Ok(()));
1517
1518        ecr_client_mock
1519            .expect_delete_repository()
1520            .with(eq(String::from("ecr_1")))
1521            .return_once(|_| Ok(()));
1522
1523        iam_client_mock
1524            .expect_delete_instance_iam_role()
1525            .with(
1526                eq(String::from("instance-role-1")),
1527                eq(vec![String::from(
1528                    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
1529                )]),
1530            )
1531            .return_once(|_, _| Ok(()));
1532
1533        ec2_client_mock
1534            .expect_delete_vpc()
1535            .with(eq(String::from("vpc-id-1")))
1536            .return_once(|_| Ok(()));
1537
1538        let graph_manager = GraphManager::new_with_clients(
1539            ec2_client_mock,
1540            iam_client_mock,
1541            ecr_client_mock,
1542            route53_client_mock,
1543        );
1544
1545        // Act
1546        let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1547
1548        // Assert
1549        assert!(destroy_result.is_ok());
1550
1551        assert_eq!(resource_graph.node_count(), 0);
1552        assert_eq!(resource_graph.edge_count(), 0);
1553    }
1554
1555    #[tokio::test]
1556    async fn test_destroy_empty_graph() {
1557        // Arrange
1558        let mut resource_graph = Graph::<Node, String>::new();
1559
1560        let ec2_client_mock = client::Ec2::default();
1561        let iam_client_mock = client::IAM::default();
1562        let ecr_client_mock = client::ECR::default();
1563        let route53_client_mock = client::Route53::default();
1564
1565        let graph_manager = GraphManager::new_with_clients(
1566            ec2_client_mock,
1567            iam_client_mock,
1568            ecr_client_mock,
1569            route53_client_mock,
1570        );
1571
1572        // Act
1573        let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1574
1575        // Assert
1576        assert!(destroy_result.is_ok());
1577
1578        assert_eq!(resource_graph.node_count(), 0);
1579        assert_eq!(resource_graph.edge_count(), 0);
1580    }
1581
1582    #[tokio::test]
1583    async fn test_destroy_resource_deletion_fails() {
1584        // Arrange
1585        let mut resource_graph = Graph::<Node, String>::new();
1586        let root = resource_graph.add_node(Node::Root);
1587        let vpc = resource_graph.add_node(Node::Resource(ResourceType::Vpc(Vpc {
1588            id: "vpc-id-1".to_string(),
1589            region: "us-west-2".to_string(),
1590            cidr_block: "10.0.0.0/16".to_string(),
1591            name: "vpc-1".to_string(),
1592        })));
1593        let subnet = resource_graph.add_node(Node::Resource(ResourceType::Subnet(Subnet {
1594            id: "subnet-id-1".to_string(),
1595            name: "vpc-1-subnet".to_string(),
1596            cidr_block: "10.0.1.0/24".to_string(),
1597            availability_zone: "us-west-2a".to_string(),
1598        })));
1599        let route_table =
1600            resource_graph.add_node(Node::Resource(ResourceType::RouteTable(RouteTable {
1601                id: "rt-id-1".to_string(),
1602            })));
1603
1604        resource_graph.extend_with_edges(&[
1605            (root, vpc, String::new()),
1606            (vpc, subnet, String::new()),
1607            (vpc, route_table, String::new()),
1608            (route_table, subnet, String::new()),
1609        ]);
1610
1611        let mut ec2_client_mock = client::Ec2::default();
1612        let iam_client_mock = client::IAM::default();
1613        let ecr_client_mock = client::ECR::default();
1614        let route53_client_mock = client::Route53::default();
1615
1616        ec2_client_mock
1617            .expect_disassociate_route_table_with_subnet()
1618            .with(eq(String::from("rt-id-1")), eq(String::from("subnet-id-1")))
1619            .return_once(|_, _| Ok(()));
1620
1621        ec2_client_mock
1622            .expect_delete_route_table()
1623            .with(eq(String::from("rt-id-1")))
1624            .return_once(|_| Ok(()));
1625
1626        ec2_client_mock
1627            .expect_delete_subnet()
1628            .with(eq(String::from("subnet-id-1")))
1629            .return_once(|_| Ok(()));
1630
1631        ec2_client_mock
1632            .expect_delete_vpc()
1633            .with(eq(String::from("vpc-id-1")))
1634            .return_once(|_| Err("VPC destruction failed".into()));
1635
1636        let graph_manager = GraphManager::new_with_clients(
1637            ec2_client_mock,
1638            iam_client_mock,
1639            ecr_client_mock,
1640            route53_client_mock,
1641        );
1642
1643        // Act
1644        let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1645
1646        // Assert
1647        assert!(destroy_result.is_err());
1648
1649        // 1 root + VPC
1650        assert_eq!(resource_graph.node_count(), 2);
1651        assert_eq!(resource_graph.edge_count(), 1);
1652
1653        let vpc_node_exists = resource_graph
1654            .node_weights()
1655            .any(|w| matches!(w, Node::Resource(ResourceType::Vpc(_))));
1656        assert!(vpc_node_exists);
1657        let subnet_node_exists = resource_graph
1658            .node_weights()
1659            .any(|w| matches!(w, Node::Resource(ResourceType::Subnet(_))));
1660        assert!(!subnet_node_exists);
1661    }
1662
1663    fn get_test_resource_graph() -> Graph<Node, String> {
1664        let mut graph = Graph::<Node, String>::new();
1665        let root = graph.add_node(Node::Root);
1666
1667        let ecr = graph.add_node(Node::Resource(ResourceType::Ecr(Ecr {
1668            id: "ecr-id-1".to_string(),
1669            name: "ecr_1".to_string(),
1670            uri: "ecr-uri-1/foo".to_string(),
1671        })));
1672
1673        let instance_role =
1674            graph.add_node(Node::Resource(ResourceType::InstanceRole(InstanceRole {
1675                name: "instance-role-1".to_string(),
1676                assume_role_policy: String::from(
1677                    r#"{ 
1678                        "Version": "2012-10-17",
1679                        "Statement": [
1680                            {
1681                                "Effect": "Allow",
1682                                "Principal": {
1683                                    "Service": "ec2.amazonaws.com"
1684                                },
1685                                "Action": "sts:AssumeRole"
1686                            }
1687                        ]
1688                    }"#,
1689                ),
1690                policy_arns: vec![String::from(
1691                    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
1692                )],
1693            })));
1694
1695        let vpc = graph.add_node(Node::Resource(ResourceType::Vpc(Vpc {
1696            id: "vpc-id-1".to_string(),
1697            region: "us-west-2".to_string(),
1698            cidr_block: "10.0.0.0/16".to_string(),
1699            name: "vpc-1".to_string(),
1700        })));
1701
1702        let security_group =
1703            graph.add_node(Node::Resource(ResourceType::SecurityGroup(SecurityGroup {
1704                id: "sg-id-1".to_string(),
1705                name: "vpc-1-security-group".to_string(),
1706                inbound_rules: vec![], // Not used in destroy
1707            })));
1708
1709        let route_table = graph.add_node(Node::Resource(ResourceType::RouteTable(RouteTable {
1710            id: "rt-id-1".to_string(),
1711        })));
1712
1713        let igw = graph.add_node(Node::Resource(ResourceType::InternetGateway(
1714            InternetGateway {
1715                id: "igw-id-1".to_string(),
1716            },
1717        )));
1718
1719        let subnet = graph.add_node(Node::Resource(ResourceType::Subnet(Subnet {
1720            id: "subnet-id-1".to_string(),
1721            name: "vpc-1-subnet".to_string(),
1722            cidr_block: "10.0.1.0/24".to_string(),
1723            availability_zone: "us-west-2a".to_string(),
1724        })));
1725
1726        let instance_profile = graph.add_node(Node::Resource(ResourceType::InstanceProfile(
1727            InstanceProfile {
1728                name: "instance_profile_1".to_string(),
1729            },
1730        )));
1731
1732        let vm = graph.add_node(Node::Resource(ResourceType::Vm(Vm {
1733            id: "vm-id-1".to_string(),
1734            public_ip: "1.2.3.4".to_string(),
1735            ami: "ami-04dd23e62ed049936".to_string(),
1736            instance_type: InstanceType::T3Micro,
1737            user_data: String::new(), // Not used in destroy
1738        })));
1739
1740        graph.extend_with_edges(&[
1741            (root, ecr, String::new()),
1742            (root, instance_role, String::new()),
1743            (root, vpc, String::new()),
1744            (vpc, security_group, String::new()),
1745            (vpc, subnet, String::new()),
1746            (vpc, route_table, String::new()),
1747            (vpc, igw, String::new()),
1748            (igw, route_table, String::new()),
1749            (route_table, subnet, String::new()),
1750            (instance_role, instance_profile, String::new()),
1751            (subnet, vm, String::new()),
1752            (instance_profile, vm, String::new()),
1753            (security_group, vm, String::new()),
1754            (ecr, vm, String::new()),
1755        ]);
1756
1757        graph
1758    }
1759
1760    #[test]
1761    fn test_kahn_traverse_empty_graph() {
1762        // Arrange
1763        let graph = Graph::<&str, String>::new();
1764
1765        // Act
1766        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1767
1768        // Assert
1769        assert!(result.is_empty());
1770    }
1771
1772    #[test]
1773    fn test_kahn_traverse_single_node() {
1774        // Arrange
1775        let mut graph = Graph::<&str, String>::new();
1776        let a = graph.add_node("a");
1777
1778        // Act
1779        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1780
1781        // Assert
1782        assert_eq!(result, vec![a]);
1783    }
1784
1785    #[test]
1786    fn test_kahn_traverse_simple_linear_graph() {
1787        // Arrange
1788        let mut graph = Graph::<&str, String>::new();
1789        let a = graph.add_node("a");
1790        let b = graph.add_node("b");
1791        let c = graph.add_node("c");
1792        graph.extend_with_edges(&[(a, b, String::new()), (b, c, String::new())]);
1793
1794        // Act
1795        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1796
1797        // Assert
1798        assert_eq!(result, vec![a, b, c]);
1799    }
1800
1801    #[test]
1802    fn test_kahn_traverse_multiple_roots() {
1803        // Arrange
1804        let mut graph = Graph::<&str, String>::new();
1805        let a = graph.add_node("a");
1806        let b = graph.add_node("b");
1807        let c = graph.add_node("c");
1808        let d = graph.add_node("d");
1809        graph.extend_with_edges(&[(a, c, String::new()), (b, d, String::new())]);
1810
1811        // Act
1812        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1813
1814        // Assert
1815        // The order of a and b is deterministic because of node_indices() order.
1816        assert_eq!(result, vec![a, b, c, d]);
1817    }
1818
1819    #[test]
1820    fn test_kahn_traverse_complex_dag() {
1821        // Arrange
1822        let mut graph = Graph::<&str, String>::new();
1823        let node_a = graph.add_node("a");
1824        let node_b = graph.add_node("b");
1825        let node_c = graph.add_node("c");
1826        let node_d = graph.add_node("d");
1827        let node_e = graph.add_node("e");
1828        let node_f = graph.add_node("f");
1829
1830        let edges = [
1831            (node_a, node_b, String::new()),
1832            (node_a, node_c, String::new()),
1833            (node_b, node_d, String::new()),
1834            (node_c, node_d, String::new()),
1835            (node_d, node_e, String::new()),
1836            (node_f, node_c, String::new()),
1837        ];
1838        graph.extend_with_edges(&edges);
1839
1840        // Act
1841        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1842
1843        // Assert
1844        assert_eq!(result.len(), 6);
1845
1846        let pos: HashMap<NodeIndex, usize> =
1847            result.iter().enumerate().map(|(i, &n)| (n, i)).collect();
1848
1849        for (u, v, _) in &edges {
1850            assert!(
1851                pos[u] < pos[v],
1852                "edge {:?} -> {:?} is not respected",
1853                graph[*u],
1854                graph[*v]
1855            );
1856        }
1857    }
1858
1859    #[test]
1860    fn test_kahn_traverse_graph_with_cycle() {
1861        // Arrange
1862        let mut graph = Graph::<&str, String>::new();
1863        let a = graph.add_node("a");
1864        let b = graph.add_node("b");
1865        let c = graph.add_node("c");
1866        graph.extend_with_edges(&[
1867            (a, b, String::new()),
1868            (b, c, String::new()),
1869            (c, a, String::new()), // Cycle a -> b -> c -> a
1870        ]);
1871
1872        // Act
1873        let result = kahn_traverse(&graph).expect_err("Graph should have a cycle");
1874
1875        // Assert
1876        assert_eq!(result.to_string(), "Cycle detected in graph");
1877    }
1878
1879    #[test]
1880    fn test_kahn_traverse_graph_with_unreachable_cycle() {
1881        // Arrange
1882        let mut graph = Graph::<&str, String>::new();
1883        let a = graph.add_node("a");
1884        let b = graph.add_node("b");
1885        let c = graph.add_node("c");
1886        let d = graph.add_node("d");
1887        graph.extend_with_edges(&[
1888            (a, b, String::new()),
1889            (c, d, String::new()),
1890            (d, c, String::new()), // Cycle c -> d -> c
1891        ]);
1892
1893        // Act
1894        let result = kahn_traverse(&graph).expect_err("Graph should have a cycle");
1895
1896        // Assert
1897        assert_eq!(result.to_string(), "Cycle detected in graph");
1898    }
1899
1900    #[test]
1901    fn test_kahn_traverse_with_removed_nodes() {
1902        // Arrange
1903        let mut graph = Graph::<&str, String>::new();
1904        let a = graph.add_node("a");
1905        let b = graph.add_node("b");
1906        let c = graph.add_node("c");
1907
1908        graph.add_edge(a, c, String::new());
1909        graph.add_edge(b, c, String::new());
1910
1911        // Remove node 'b' (index 1) to create a hole in indices: 0, 2
1912        graph.remove_node(b);
1913
1914        // Act
1915        let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1916        let result_weights: Vec<&str> = result
1917            .iter()
1918            .map(|&i| {
1919                *graph
1920                    .node_weight(i)
1921                    .expect("Node weight must exist for traversed indices")
1922            })
1923            .collect();
1924
1925        // Assert
1926        // Expected order: "a" -> "c"
1927        assert_eq!(result_weights, vec!["a", "c"]);
1928    }
1929}