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 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 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 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 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 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 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 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 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 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 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
1034pub fn kahn_traverse<T>(
1036 graph: &Graph<T, String>,
1037) -> Result<Vec<NodeIndex>, Box<dyn std::error::Error>> {
1038 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 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 while let Some(node) = queue.pop_front() {
1054 result.push(node);
1055
1056 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 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 let instance_type = InstanceType::T3Micro;
1087 let domain_name = None;
1088
1089 let graph = GraphManager::get_spec_graph(&instance_type, domain_name);
1091
1092 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 let instance_type = InstanceType::T3Micro;
1108 let domain_name = Some(String::from("example.com"));
1109
1110 let graph = GraphManager::get_spec_graph(&instance_type, domain_name);
1112
1113 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 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 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 let (resource_graph, vm, ecr) = graph_manager
1308 .deploy_spec_graph(&spec_graph)
1309 .await
1310 .expect("Failed to deploy");
1311
1312 assert_eq!(resource_graph.node_count(), 10); 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 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 let (resource_graph, vm, ecr) = graph_manager
1371 .deploy_spec_graph(&spec_graph)
1372 .await
1373 .expect("Failed to deploy");
1374
1375 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 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 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 let (resource_graph, vm, ecr) = graph_manager
1434 .deploy_spec_graph(&spec_graph)
1435 .await
1436 .expect("Failed to deploy");
1437
1438 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 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 ec2_client_mock
1467 .expect_terminate_instance()
1468 .with(eq(String::from("vm-id-1")))
1469 .return_once(|_| Ok(()));
1470
1471 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 let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1547
1548 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 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 let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1574
1575 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 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 let destroy_result = graph_manager.destroy(&mut resource_graph).await;
1645
1646 assert!(destroy_result.is_err());
1648
1649 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![], })));
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(), })));
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 let graph = Graph::<&str, String>::new();
1764
1765 let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1767
1768 assert!(result.is_empty());
1770 }
1771
1772 #[test]
1773 fn test_kahn_traverse_single_node() {
1774 let mut graph = Graph::<&str, String>::new();
1776 let a = graph.add_node("a");
1777
1778 let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1780
1781 assert_eq!(result, vec![a]);
1783 }
1784
1785 #[test]
1786 fn test_kahn_traverse_simple_linear_graph() {
1787 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 let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1796
1797 assert_eq!(result, vec![a, b, c]);
1799 }
1800
1801 #[test]
1802 fn test_kahn_traverse_multiple_roots() {
1803 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 let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1813
1814 assert_eq!(result, vec![a, b, c, d]);
1817 }
1818
1819 #[test]
1820 fn test_kahn_traverse_complex_dag() {
1821 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 let result = kahn_traverse(&graph).expect("Failed to traverse graph");
1842
1843 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 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()), ]);
1871
1872 let result = kahn_traverse(&graph).expect_err("Graph should have a cycle");
1874
1875 assert_eq!(result.to_string(), "Cycle detected in graph");
1877 }
1878
1879 #[test]
1880 fn test_kahn_traverse_graph_with_unreachable_cycle() {
1881 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()), ]);
1892
1893 let result = kahn_traverse(&graph).expect_err("Graph should have a cycle");
1895
1896 assert_eq!(result.to_string(), "Cycle detected in graph");
1898 }
1899
1900 #[test]
1901 fn test_kahn_traverse_with_removed_nodes() {
1902 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 graph.remove_node(b);
1913
1914 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_eq!(result_weights, vec!["a", "c"]);
1928 }
1929}