terraformgraph 1.0.1__tar.gz → 1.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {terraformgraph-1.0.1/terraformgraph.egg-info → terraformgraph-1.0.3}/PKG-INFO +7 -5
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/README.md +5 -3
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/pyproject.toml +2 -2
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/__init__.py +1 -1
- terraformgraph-1.0.3/terraformgraph/aggregator.py +140 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/layout.py +2 -2
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/main.py +16 -10
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/parser.py +6 -3
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/renderer.py +23 -3
- {terraformgraph-1.0.1 → terraformgraph-1.0.3/terraformgraph.egg-info}/PKG-INFO +7 -5
- terraformgraph-1.0.1/terraformgraph/aggregator.py +0 -396
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/LICENSE +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/setup.cfg +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/__main__.py +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/config/aggregation_rules.yaml +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/config/logical_connections.yaml +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/config_loader.py +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/icons.py +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph.egg-info/SOURCES.txt +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph.egg-info/dependency_links.txt +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph.egg-info/entry_points.txt +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph.egg-info/requires.txt +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph.egg-info/top_level.txt +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/tests/test_config_loader.py +0 -0
- {terraformgraph-1.0.1 → terraformgraph-1.0.3}/tests/test_integration.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: terraformgraph
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Generate interactive architecture diagrams from Terraform configurations
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Ferdinando Bonsegna <1bonsegnaferdinando@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/ferdinandobons/terraformgraph
|
|
8
8
|
Project-URL: Documentation, https://github.com/ferdinandobons/terraformgraph#readme
|
|
@@ -75,9 +75,11 @@ pip install -e .
|
|
|
75
75
|
Generate a diagram from a Terraform directory:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
terraformgraph -t ./infrastructure
|
|
78
|
+
terraformgraph -t ./infrastructure
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
This creates `diagram.html` in the current directory.
|
|
82
|
+
|
|
81
83
|
### With Environment Subdirectories
|
|
82
84
|
|
|
83
85
|
If your Terraform is organized by environment:
|
|
@@ -91,7 +93,7 @@ terraformgraph -t ./infrastructure -e prod -o prod-diagram.html
|
|
|
91
93
|
For beautiful AWS service icons, download the [AWS Architecture Icons](https://aws.amazon.com/architecture/icons/) and extract them:
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
96
|
+
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
## Command Line Options
|
|
@@ -101,7 +103,7 @@ terraformgraph -t ./infrastructure -i ./AWS_Icons -o diagram.html
|
|
|
101
103
|
| `-t, --terraform` | Yes | Path to Terraform directory |
|
|
102
104
|
| `-e, --environment` | No | Environment subdirectory (dev, staging, prod) |
|
|
103
105
|
| `-i, --icons` | No | Path to AWS icons directory |
|
|
104
|
-
| `-o, --output` |
|
|
106
|
+
| `-o, --output` | No | Output HTML file path (default: `diagram.html`) |
|
|
105
107
|
| `-v, --verbose` | No | Enable debug output |
|
|
106
108
|
|
|
107
109
|
## Configuration
|
|
@@ -37,9 +37,11 @@ pip install -e .
|
|
|
37
37
|
Generate a diagram from a Terraform directory:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
terraformgraph -t ./infrastructure
|
|
40
|
+
terraformgraph -t ./infrastructure
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
This creates `diagram.html` in the current directory.
|
|
44
|
+
|
|
43
45
|
### With Environment Subdirectories
|
|
44
46
|
|
|
45
47
|
If your Terraform is organized by environment:
|
|
@@ -53,7 +55,7 @@ terraformgraph -t ./infrastructure -e prod -o prod-diagram.html
|
|
|
53
55
|
For beautiful AWS service icons, download the [AWS Architecture Icons](https://aws.amazon.com/architecture/icons/) and extract them:
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
58
|
+
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
## Command Line Options
|
|
@@ -63,7 +65,7 @@ terraformgraph -t ./infrastructure -i ./AWS_Icons -o diagram.html
|
|
|
63
65
|
| `-t, --terraform` | Yes | Path to Terraform directory |
|
|
64
66
|
| `-e, --environment` | No | Environment subdirectory (dev, staging, prod) |
|
|
65
67
|
| `-i, --icons` | No | Path to AWS icons directory |
|
|
66
|
-
| `-o, --output` |
|
|
68
|
+
| `-o, --output` | No | Output HTML file path (default: `diagram.html`) |
|
|
67
69
|
| `-v, --verbose` | No | Enable debug output |
|
|
68
70
|
|
|
69
71
|
## Configuration
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "terraformgraph"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Generate interactive architecture diagrams from Terraform configurations"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [
|
|
13
|
-
{name = "
|
|
13
|
+
{name = "Ferdinando Bonsegna", email = "1bonsegnaferdinando@gmail.com"}
|
|
14
14
|
]
|
|
15
15
|
keywords = [
|
|
16
16
|
"terraform",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource Aggregator
|
|
3
|
+
|
|
4
|
+
Aggregates low-level Terraform resources into high-level logical services
|
|
5
|
+
for cleaner architecture diagrams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .config_loader import ConfigLoader
|
|
12
|
+
from .parser import ParseResult, TerraformResource
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LogicalService:
|
|
17
|
+
"""A high-level logical service aggregating multiple resources."""
|
|
18
|
+
service_type: str # e.g., 'alb', 'ecs', 's3', 'sqs'
|
|
19
|
+
name: str
|
|
20
|
+
icon_resource_type: str # The Terraform type to use for the icon
|
|
21
|
+
resources: List[TerraformResource] = field(default_factory=list)
|
|
22
|
+
count: int = 1 # How many instances (e.g., 24 SQS queues)
|
|
23
|
+
is_vpc_resource: bool = False
|
|
24
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def id(self) -> str:
|
|
28
|
+
return f"{self.service_type}.{self.name}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class LogicalConnection:
|
|
33
|
+
"""A connection between logical services."""
|
|
34
|
+
source_id: str
|
|
35
|
+
target_id: str
|
|
36
|
+
label: Optional[str] = None
|
|
37
|
+
connection_type: str = 'default' # 'default', 'data_flow', 'trigger', 'encrypt'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AggregatedResult:
|
|
42
|
+
"""Result of aggregating resources into logical services."""
|
|
43
|
+
services: List[LogicalService] = field(default_factory=list)
|
|
44
|
+
connections: List[LogicalConnection] = field(default_factory=list)
|
|
45
|
+
vpc_services: List[LogicalService] = field(default_factory=list)
|
|
46
|
+
global_services: List[LogicalService] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ResourceAggregator:
|
|
50
|
+
"""Aggregates Terraform resources into logical services."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config_loader: Optional[ConfigLoader] = None):
|
|
53
|
+
self._config = config_loader or ConfigLoader()
|
|
54
|
+
self._aggregation_rules = self._build_aggregation_rules()
|
|
55
|
+
self._logical_connections = self._config.get_logical_connections()
|
|
56
|
+
self._build_type_to_rule_map()
|
|
57
|
+
|
|
58
|
+
def _build_aggregation_rules(self) -> Dict[str, Dict[str, Any]]:
|
|
59
|
+
"""Build aggregation rules dict from config."""
|
|
60
|
+
flat_rules = self._config.get_flat_aggregation_rules()
|
|
61
|
+
result = {}
|
|
62
|
+
for service_name, config in flat_rules.items():
|
|
63
|
+
# Map YAML format (primary/secondary/in_vpc) to internal format
|
|
64
|
+
result[service_name] = {
|
|
65
|
+
'primary': config.get("primary", []),
|
|
66
|
+
'aggregate': config.get("secondary", []), # secondary in YAML -> aggregate internally
|
|
67
|
+
'icon': config.get("primary", [""])[0] if config.get("primary") else "",
|
|
68
|
+
'display_name': service_name.replace("_", " ").title(),
|
|
69
|
+
'is_vpc': config.get("in_vpc", False),
|
|
70
|
+
}
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def _build_type_to_rule_map(self) -> None:
|
|
74
|
+
"""Build a mapping from resource type to aggregation rule."""
|
|
75
|
+
self._type_to_rule: Dict[str, str] = {}
|
|
76
|
+
for rule_name, rule in self._aggregation_rules.items():
|
|
77
|
+
for res_type in rule['primary']:
|
|
78
|
+
self._type_to_rule[res_type] = rule_name
|
|
79
|
+
for res_type in rule['aggregate']:
|
|
80
|
+
self._type_to_rule[res_type] = rule_name
|
|
81
|
+
|
|
82
|
+
def aggregate(self, parse_result: ParseResult) -> AggregatedResult:
|
|
83
|
+
"""Aggregate parsed resources into logical services."""
|
|
84
|
+
result = AggregatedResult()
|
|
85
|
+
|
|
86
|
+
# Group resources by aggregation rule
|
|
87
|
+
rule_resources: Dict[str, List[TerraformResource]] = {}
|
|
88
|
+
unmatched: List[TerraformResource] = []
|
|
89
|
+
|
|
90
|
+
for resource in parse_result.resources:
|
|
91
|
+
rule_name = self._type_to_rule.get(resource.resource_type)
|
|
92
|
+
if rule_name:
|
|
93
|
+
rule_resources.setdefault(rule_name, []).append(resource)
|
|
94
|
+
else:
|
|
95
|
+
unmatched.append(resource)
|
|
96
|
+
|
|
97
|
+
# Create logical services from grouped resources
|
|
98
|
+
for rule_name, resources in rule_resources.items():
|
|
99
|
+
rule = self._aggregation_rules[rule_name]
|
|
100
|
+
|
|
101
|
+
# Count primary resources
|
|
102
|
+
primary_count = sum(1 for r in resources if r.resource_type in rule['primary'])
|
|
103
|
+
if primary_count == 0:
|
|
104
|
+
continue # Skip if no primary resources
|
|
105
|
+
|
|
106
|
+
service = LogicalService(
|
|
107
|
+
service_type=rule_name,
|
|
108
|
+
name=rule['display_name'],
|
|
109
|
+
icon_resource_type=rule['icon'],
|
|
110
|
+
resources=resources,
|
|
111
|
+
count=primary_count,
|
|
112
|
+
is_vpc_resource=rule['is_vpc'],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
result.services.append(service)
|
|
116
|
+
if service.is_vpc_resource:
|
|
117
|
+
result.vpc_services.append(service)
|
|
118
|
+
else:
|
|
119
|
+
result.global_services.append(service)
|
|
120
|
+
|
|
121
|
+
# Create logical connections based on which services exist
|
|
122
|
+
existing_services = {s.service_type for s in result.services}
|
|
123
|
+
for conn in self._logical_connections:
|
|
124
|
+
source = conn.get("source", "")
|
|
125
|
+
target = conn.get("target", "")
|
|
126
|
+
if source in existing_services and target in existing_services:
|
|
127
|
+
result.connections.append(LogicalConnection(
|
|
128
|
+
source_id=f"{source}.{self._aggregation_rules[source]['display_name']}",
|
|
129
|
+
target_id=f"{target}.{self._aggregation_rules[target]['display_name']}",
|
|
130
|
+
label=conn.get("label", ""),
|
|
131
|
+
connection_type=conn.get("type", "default"),
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def aggregate_resources(parse_result: ParseResult) -> AggregatedResult:
|
|
138
|
+
"""Convenience function to aggregate resources."""
|
|
139
|
+
aggregator = ResourceAggregator()
|
|
140
|
+
return aggregator.aggregate(parse_result)
|
|
@@ -88,13 +88,13 @@ class LayoutEngine:
|
|
|
88
88
|
st = service.service_type
|
|
89
89
|
if st in ('cloudfront', 'waf', 'route53', 'acm', 'cognito'):
|
|
90
90
|
edge_services.append(service)
|
|
91
|
-
elif st in ('alb', 'ecs', 'ec2', 'security', 'vpc'):
|
|
91
|
+
elif st in ('alb', 'ecs', 'ec2', 'security_groups', 'security', 'vpc'):
|
|
92
92
|
vpc_services.append(service)
|
|
93
93
|
elif st in ('s3', 'dynamodb', 'mongodb'):
|
|
94
94
|
data_services.append(service)
|
|
95
95
|
elif st in ('sqs', 'sns', 'eventbridge'):
|
|
96
96
|
messaging_services.append(service)
|
|
97
|
-
elif st in ('kms', 'secrets', 'iam'):
|
|
97
|
+
elif st in ('kms', 'secrets', 'secrets_manager', 'iam'):
|
|
98
98
|
security_services.append(service)
|
|
99
99
|
else:
|
|
100
100
|
other_services.append(service)
|
|
@@ -6,14 +6,17 @@ Generates AWS infrastructure diagrams from Terraform code using official AWS ico
|
|
|
6
6
|
Creates high-level architectural diagrams with logical service groupings.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
# Parse a directory directly (
|
|
10
|
-
terraformgraph -t ./infrastructure
|
|
9
|
+
# Parse a directory directly (generates diagram.html by default)
|
|
10
|
+
terraformgraph -t ./infrastructure
|
|
11
11
|
|
|
12
12
|
# Parse a specific environment subdirectory
|
|
13
|
-
terraformgraph -t ./infrastructure -e dev
|
|
13
|
+
terraformgraph -t ./infrastructure -e dev
|
|
14
|
+
|
|
15
|
+
# With custom output path
|
|
16
|
+
terraformgraph -t ./infrastructure -o my-diagram.html
|
|
14
17
|
|
|
15
18
|
# With custom icons path
|
|
16
|
-
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
19
|
+
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
import argparse
|
|
@@ -33,14 +36,17 @@ def main():
|
|
|
33
36
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
34
37
|
epilog='''
|
|
35
38
|
Examples:
|
|
36
|
-
# Parse a directory (
|
|
37
|
-
terraformgraph -t ./infrastructure
|
|
39
|
+
# Parse a directory (generates diagram.html by default)
|
|
40
|
+
terraformgraph -t ./infrastructure
|
|
38
41
|
|
|
39
42
|
# Parse a specific environment subdirectory
|
|
40
|
-
terraformgraph -t ./infrastructure -e dev
|
|
43
|
+
terraformgraph -t ./infrastructure -e dev
|
|
44
|
+
|
|
45
|
+
# With custom output path
|
|
46
|
+
terraformgraph -t ./infrastructure -o my-diagram.html
|
|
41
47
|
|
|
42
48
|
# With custom icons path
|
|
43
|
-
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
49
|
+
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
44
50
|
'''
|
|
45
51
|
)
|
|
46
52
|
|
|
@@ -64,8 +70,8 @@ Examples:
|
|
|
64
70
|
|
|
65
71
|
parser.add_argument(
|
|
66
72
|
'-o', '--output',
|
|
67
|
-
|
|
68
|
-
help='Output file path (HTML)'
|
|
73
|
+
default='diagram.html',
|
|
74
|
+
help='Output file path (HTML). Default: diagram.html'
|
|
69
75
|
)
|
|
70
76
|
|
|
71
77
|
parser.add_argument(
|
|
@@ -4,6 +4,7 @@ Terraform HCL Parser
|
|
|
4
4
|
Parses Terraform files and extracts AWS resources and their relationships.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
import re
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from pathlib import Path
|
|
@@ -11,6 +12,8 @@ from typing import Any, Dict, List, Optional
|
|
|
11
12
|
|
|
12
13
|
import hcl2
|
|
13
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
@dataclass
|
|
16
19
|
class TerraformResource:
|
|
@@ -130,7 +133,7 @@ class TerraformParser:
|
|
|
130
133
|
# Parse all .tf files in directory
|
|
131
134
|
tf_files = list(directory.glob("*.tf"))
|
|
132
135
|
if not tf_files:
|
|
133
|
-
|
|
136
|
+
logger.warning("No .tf files found in %s", directory)
|
|
134
137
|
|
|
135
138
|
for tf_file in tf_files:
|
|
136
139
|
self._parse_file(tf_file, result, module_path="")
|
|
@@ -153,7 +156,7 @@ class TerraformParser:
|
|
|
153
156
|
with open(file_path, 'r') as f:
|
|
154
157
|
content = hcl2.load(f)
|
|
155
158
|
except Exception as e:
|
|
156
|
-
|
|
159
|
+
logger.warning("Could not parse %s: %s", file_path, e)
|
|
157
160
|
return
|
|
158
161
|
|
|
159
162
|
# Extract resources
|
|
@@ -199,7 +202,7 @@ class TerraformParser:
|
|
|
199
202
|
module_path = self.infrastructure_path / '.modules' / source
|
|
200
203
|
|
|
201
204
|
if not module_path.exists():
|
|
202
|
-
|
|
205
|
+
logger.warning("Module path not found: %s", module_path)
|
|
203
206
|
return ParseResult()
|
|
204
207
|
|
|
205
208
|
# Check cache
|
|
@@ -250,7 +250,7 @@ class SVGRenderer:
|
|
|
250
250
|
data-target="{html.escape(connection.target_id)}"
|
|
251
251
|
data-conn-type="{connection.connection_type}"
|
|
252
252
|
data-label="{html.escape(label)}">
|
|
253
|
-
<path class="connection-hitarea" d="{path}"/>
|
|
253
|
+
<path class="connection-hitarea" d="{path}" fill="none" stroke="transparent" stroke-width="15"/>
|
|
254
254
|
<path class="connection-path" d="{path}" fill="none" stroke="{stroke_color}"
|
|
255
255
|
stroke-width="1.5" {dash_attr} marker-end="{marker}" opacity="0.7"/>
|
|
256
256
|
</g>
|
|
@@ -676,6 +676,11 @@ class HTMLRenderer:
|
|
|
676
676
|
|
|
677
677
|
function startDrag(e) {{
|
|
678
678
|
e.preventDefault();
|
|
679
|
+
|
|
680
|
+
// Guard against null CTM (can happen during rendering)
|
|
681
|
+
const ctm = svg.getScreenCTM();
|
|
682
|
+
if (!ctm) return;
|
|
683
|
+
|
|
679
684
|
dragging = e.currentTarget;
|
|
680
685
|
dragging.classList.add('dragging');
|
|
681
686
|
dragging.style.cursor = 'grabbing';
|
|
@@ -683,7 +688,15 @@ class HTMLRenderer:
|
|
|
683
688
|
const pt = svg.createSVGPoint();
|
|
684
689
|
pt.x = e.clientX;
|
|
685
690
|
pt.y = e.clientY;
|
|
686
|
-
const svgP = pt.matrixTransform(
|
|
691
|
+
const svgP = pt.matrixTransform(ctm.inverse());
|
|
692
|
+
|
|
693
|
+
// Validate coordinates to prevent NaN issues
|
|
694
|
+
if (isNaN(svgP.x) || isNaN(svgP.y)) {{
|
|
695
|
+
dragging.classList.remove('dragging');
|
|
696
|
+
dragging.style.cursor = 'grab';
|
|
697
|
+
dragging = null;
|
|
698
|
+
return;
|
|
699
|
+
}}
|
|
687
700
|
|
|
688
701
|
const id = dragging.dataset.serviceId;
|
|
689
702
|
const pos = servicePositions[id] || {{ x: 0, y: 0 }};
|
|
@@ -697,10 +710,17 @@ class HTMLRenderer:
|
|
|
697
710
|
function drag(e) {{
|
|
698
711
|
if (!dragging) return;
|
|
699
712
|
|
|
713
|
+
// Guard against null CTM
|
|
714
|
+
const ctm = svg.getScreenCTM();
|
|
715
|
+
if (!ctm) return;
|
|
716
|
+
|
|
700
717
|
const pt = svg.createSVGPoint();
|
|
701
718
|
pt.x = e.clientX;
|
|
702
719
|
pt.y = e.clientY;
|
|
703
|
-
const svgP = pt.matrixTransform(
|
|
720
|
+
const svgP = pt.matrixTransform(ctm.inverse());
|
|
721
|
+
|
|
722
|
+
// Validate coordinates to prevent NaN issues
|
|
723
|
+
if (isNaN(svgP.x) || isNaN(svgP.y)) return;
|
|
704
724
|
|
|
705
725
|
let newX = svgP.x - offset.x;
|
|
706
726
|
let newY = svgP.y - offset.y;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: terraformgraph
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Generate interactive architecture diagrams from Terraform configurations
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Ferdinando Bonsegna <1bonsegnaferdinando@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/ferdinandobons/terraformgraph
|
|
8
8
|
Project-URL: Documentation, https://github.com/ferdinandobons/terraformgraph#readme
|
|
@@ -75,9 +75,11 @@ pip install -e .
|
|
|
75
75
|
Generate a diagram from a Terraform directory:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
terraformgraph -t ./infrastructure
|
|
78
|
+
terraformgraph -t ./infrastructure
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
This creates `diagram.html` in the current directory.
|
|
82
|
+
|
|
81
83
|
### With Environment Subdirectories
|
|
82
84
|
|
|
83
85
|
If your Terraform is organized by environment:
|
|
@@ -91,7 +93,7 @@ terraformgraph -t ./infrastructure -e prod -o prod-diagram.html
|
|
|
91
93
|
For beautiful AWS service icons, download the [AWS Architecture Icons](https://aws.amazon.com/architecture/icons/) and extract them:
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
|
-
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
96
|
+
terraformgraph -t ./infrastructure -i ./AWS_Icons
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
## Command Line Options
|
|
@@ -101,7 +103,7 @@ terraformgraph -t ./infrastructure -i ./AWS_Icons -o diagram.html
|
|
|
101
103
|
| `-t, --terraform` | Yes | Path to Terraform directory |
|
|
102
104
|
| `-e, --environment` | No | Environment subdirectory (dev, staging, prod) |
|
|
103
105
|
| `-i, --icons` | No | Path to AWS icons directory |
|
|
104
|
-
| `-o, --output` |
|
|
106
|
+
| `-o, --output` | No | Output HTML file path (default: `diagram.html`) |
|
|
105
107
|
| `-v, --verbose` | No | Enable debug output |
|
|
106
108
|
|
|
107
109
|
## Configuration
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Resource Aggregator
|
|
3
|
-
|
|
4
|
-
Aggregates low-level Terraform resources into high-level logical services
|
|
5
|
-
for cleaner architecture diagrams.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Any, Dict, List, Optional
|
|
10
|
-
|
|
11
|
-
from .config_loader import ConfigLoader
|
|
12
|
-
from .parser import ParseResult, TerraformResource
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass
|
|
16
|
-
class LogicalService:
|
|
17
|
-
"""A high-level logical service aggregating multiple resources."""
|
|
18
|
-
service_type: str # e.g., 'alb', 'ecs', 's3', 'sqs'
|
|
19
|
-
name: str
|
|
20
|
-
icon_resource_type: str # The Terraform type to use for the icon
|
|
21
|
-
resources: List[TerraformResource] = field(default_factory=list)
|
|
22
|
-
count: int = 1 # How many instances (e.g., 24 SQS queues)
|
|
23
|
-
is_vpc_resource: bool = False
|
|
24
|
-
attributes: Dict[str, str] = field(default_factory=dict)
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def id(self) -> str:
|
|
28
|
-
return f"{self.service_type}.{self.name}"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass
|
|
32
|
-
class LogicalConnection:
|
|
33
|
-
"""A connection between logical services."""
|
|
34
|
-
source_id: str
|
|
35
|
-
target_id: str
|
|
36
|
-
label: Optional[str] = None
|
|
37
|
-
connection_type: str = 'default' # 'default', 'data_flow', 'trigger', 'encrypt'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@dataclass
|
|
41
|
-
class AggregatedResult:
|
|
42
|
-
"""Result of aggregating resources into logical services."""
|
|
43
|
-
services: List[LogicalService] = field(default_factory=list)
|
|
44
|
-
connections: List[LogicalConnection] = field(default_factory=list)
|
|
45
|
-
vpc_services: List[LogicalService] = field(default_factory=list)
|
|
46
|
-
global_services: List[LogicalService] = field(default_factory=list)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Define which resource types should be aggregated together
|
|
50
|
-
AGGREGATION_RULES = {
|
|
51
|
-
# Load Balancing: ALB + listeners + target groups = one ALB
|
|
52
|
-
'alb': {
|
|
53
|
-
'primary': ['aws_lb'],
|
|
54
|
-
'aggregate': ['aws_lb_listener', 'aws_lb_target_group', 'aws_lb_target_group_attachment'],
|
|
55
|
-
'icon': 'aws_lb',
|
|
56
|
-
'display_name': 'Load Balancer',
|
|
57
|
-
'is_vpc': True,
|
|
58
|
-
},
|
|
59
|
-
# ECS: cluster + services + task definitions = one ECS
|
|
60
|
-
'ecs': {
|
|
61
|
-
'primary': ['aws_ecs_cluster'],
|
|
62
|
-
'aggregate': ['aws_ecs_service', 'aws_ecs_task_definition'],
|
|
63
|
-
'icon': 'aws_ecs_cluster',
|
|
64
|
-
'display_name': 'ECS Cluster',
|
|
65
|
-
'is_vpc': True,
|
|
66
|
-
},
|
|
67
|
-
# VPC: vpc + subnets + gateways + route tables = one VPC
|
|
68
|
-
'vpc': {
|
|
69
|
-
'primary': ['aws_vpc'],
|
|
70
|
-
'aggregate': ['aws_subnet', 'aws_internet_gateway', 'aws_nat_gateway',
|
|
71
|
-
'aws_route_table', 'aws_route', 'aws_route_table_association',
|
|
72
|
-
'aws_eip', 'aws_vpc_endpoint', 'aws_db_subnet_group'],
|
|
73
|
-
'icon': 'aws_vpc',
|
|
74
|
-
'display_name': 'VPC',
|
|
75
|
-
'is_vpc': True,
|
|
76
|
-
},
|
|
77
|
-
# Security Groups: aggregate all SGs
|
|
78
|
-
'security': {
|
|
79
|
-
'primary': ['aws_security_group'],
|
|
80
|
-
'aggregate': ['aws_security_group_rule'],
|
|
81
|
-
'icon': 'aws_security_group',
|
|
82
|
-
'display_name': 'Security Groups',
|
|
83
|
-
'is_vpc': True,
|
|
84
|
-
},
|
|
85
|
-
# S3: buckets (aggregate policies, versioning, etc.)
|
|
86
|
-
's3': {
|
|
87
|
-
'primary': ['aws_s3_bucket'],
|
|
88
|
-
'aggregate': ['aws_s3_bucket_policy', 'aws_s3_bucket_versioning',
|
|
89
|
-
'aws_s3_bucket_lifecycle_configuration', 'aws_s3_bucket_notification',
|
|
90
|
-
'aws_s3_bucket_cors_configuration', 'aws_s3_bucket_public_access_block',
|
|
91
|
-
'aws_s3_bucket_ownership_controls', 'aws_s3_bucket_server_side_encryption_configuration'],
|
|
92
|
-
'icon': 'aws_s3_bucket',
|
|
93
|
-
'display_name': 'S3 Buckets',
|
|
94
|
-
'is_vpc': False,
|
|
95
|
-
},
|
|
96
|
-
# SQS: aggregate all queues
|
|
97
|
-
'sqs': {
|
|
98
|
-
'primary': ['aws_sqs_queue'],
|
|
99
|
-
'aggregate': ['aws_sqs_queue_policy'],
|
|
100
|
-
'icon': 'aws_sqs_queue',
|
|
101
|
-
'display_name': 'SQS Queues',
|
|
102
|
-
'is_vpc': False,
|
|
103
|
-
},
|
|
104
|
-
# SNS: aggregate topics
|
|
105
|
-
'sns': {
|
|
106
|
-
'primary': ['aws_sns_topic'],
|
|
107
|
-
'aggregate': ['aws_sns_topic_policy', 'aws_sns_topic_subscription'],
|
|
108
|
-
'icon': 'aws_sns_topic',
|
|
109
|
-
'display_name': 'SNS Topics',
|
|
110
|
-
'is_vpc': False,
|
|
111
|
-
},
|
|
112
|
-
# Cognito: user pool + clients + domain
|
|
113
|
-
'cognito': {
|
|
114
|
-
'primary': ['aws_cognito_user_pool'],
|
|
115
|
-
'aggregate': ['aws_cognito_user_pool_client', 'aws_cognito_user_pool_domain',
|
|
116
|
-
'aws_cognito_identity_pool', 'aws_cognito_identity_pool_roles_attachment',
|
|
117
|
-
'aws_cognito_log_delivery_configuration'],
|
|
118
|
-
'icon': 'aws_cognito_user_pool',
|
|
119
|
-
'display_name': 'Cognito',
|
|
120
|
-
'is_vpc': False,
|
|
121
|
-
},
|
|
122
|
-
# KMS: keys + aliases
|
|
123
|
-
'kms': {
|
|
124
|
-
'primary': ['aws_kms_key'],
|
|
125
|
-
'aggregate': ['aws_kms_alias'],
|
|
126
|
-
'icon': 'aws_kms_key',
|
|
127
|
-
'display_name': 'KMS Keys',
|
|
128
|
-
'is_vpc': False,
|
|
129
|
-
},
|
|
130
|
-
# Secrets Manager
|
|
131
|
-
'secrets': {
|
|
132
|
-
'primary': ['aws_secretsmanager_secret'],
|
|
133
|
-
'aggregate': ['aws_secretsmanager_secret_version'],
|
|
134
|
-
'icon': 'aws_secretsmanager_secret',
|
|
135
|
-
'display_name': 'Secrets Manager',
|
|
136
|
-
'is_vpc': False,
|
|
137
|
-
},
|
|
138
|
-
# Route53
|
|
139
|
-
'route53': {
|
|
140
|
-
'primary': ['aws_route53_zone'],
|
|
141
|
-
'aggregate': ['aws_route53_record'],
|
|
142
|
-
'icon': 'aws_route53_zone',
|
|
143
|
-
'display_name': 'Route 53',
|
|
144
|
-
'is_vpc': False,
|
|
145
|
-
},
|
|
146
|
-
# ACM Certificates
|
|
147
|
-
'acm': {
|
|
148
|
-
'primary': ['aws_acm_certificate'],
|
|
149
|
-
'aggregate': ['aws_acm_certificate_validation'],
|
|
150
|
-
'icon': 'aws_acm_certificate',
|
|
151
|
-
'display_name': 'Certificates',
|
|
152
|
-
'is_vpc': False,
|
|
153
|
-
},
|
|
154
|
-
# CloudWatch
|
|
155
|
-
'cloudwatch': {
|
|
156
|
-
'primary': ['aws_cloudwatch_log_group', 'aws_cloudwatch_metric_alarm'],
|
|
157
|
-
'aggregate': ['aws_cloudwatch_log_resource_policy', 'aws_cloudwatch_log_delivery',
|
|
158
|
-
'aws_cloudwatch_log_delivery_source', 'aws_cloudwatch_log_delivery_destination',
|
|
159
|
-
'aws_cloudwatch_dashboard'],
|
|
160
|
-
'icon': 'aws_cloudwatch_metric_alarm',
|
|
161
|
-
'display_name': 'CloudWatch',
|
|
162
|
-
'is_vpc': False,
|
|
163
|
-
},
|
|
164
|
-
# EventBridge
|
|
165
|
-
'eventbridge': {
|
|
166
|
-
'primary': ['aws_cloudwatch_event_rule', 'aws_cloudwatch_event_bus'],
|
|
167
|
-
'aggregate': ['aws_cloudwatch_event_target', 'aws_cloudwatch_event_archive'],
|
|
168
|
-
'icon': 'aws_cloudwatch_event_rule',
|
|
169
|
-
'display_name': 'EventBridge',
|
|
170
|
-
'is_vpc': False,
|
|
171
|
-
},
|
|
172
|
-
# WAF
|
|
173
|
-
'waf': {
|
|
174
|
-
'primary': ['aws_wafv2_web_acl'],
|
|
175
|
-
'aggregate': ['aws_wafv2_web_acl_association', 'aws_wafv2_rule_group'],
|
|
176
|
-
'icon': 'aws_wafv2_web_acl',
|
|
177
|
-
'display_name': 'WAF',
|
|
178
|
-
'is_vpc': False,
|
|
179
|
-
},
|
|
180
|
-
# IAM
|
|
181
|
-
'iam': {
|
|
182
|
-
'primary': ['aws_iam_role'],
|
|
183
|
-
'aggregate': ['aws_iam_policy', 'aws_iam_role_policy', 'aws_iam_role_policy_attachment',
|
|
184
|
-
'aws_iam_instance_profile'],
|
|
185
|
-
'icon': 'aws_iam_role',
|
|
186
|
-
'display_name': 'IAM Roles',
|
|
187
|
-
'is_vpc': False,
|
|
188
|
-
},
|
|
189
|
-
# ECR
|
|
190
|
-
'ecr': {
|
|
191
|
-
'primary': ['aws_ecr_repository'],
|
|
192
|
-
'aggregate': [],
|
|
193
|
-
'icon': 'aws_ecr_repository',
|
|
194
|
-
'display_name': 'ECR',
|
|
195
|
-
'is_vpc': False,
|
|
196
|
-
},
|
|
197
|
-
# DynamoDB
|
|
198
|
-
'dynamodb': {
|
|
199
|
-
'primary': ['aws_dynamodb_table'],
|
|
200
|
-
'aggregate': [],
|
|
201
|
-
'icon': 'aws_dynamodb_table',
|
|
202
|
-
'display_name': 'DynamoDB',
|
|
203
|
-
'is_vpc': False,
|
|
204
|
-
},
|
|
205
|
-
# SES
|
|
206
|
-
'ses': {
|
|
207
|
-
'primary': ['aws_ses_domain_identity'],
|
|
208
|
-
'aggregate': ['aws_ses_domain_dkim', 'aws_ses_domain_mail_from',
|
|
209
|
-
'aws_ses_identity_notification_topic', 'aws_ses_configuration_set'],
|
|
210
|
-
'icon': 'aws_ses_domain_identity',
|
|
211
|
-
'display_name': 'SES',
|
|
212
|
-
'is_vpc': False,
|
|
213
|
-
},
|
|
214
|
-
# CloudFront
|
|
215
|
-
'cloudfront': {
|
|
216
|
-
'primary': ['aws_cloudfront_distribution'],
|
|
217
|
-
'aggregate': ['aws_cloudfront_origin_access_control'],
|
|
218
|
-
'icon': 'aws_cloudfront_distribution',
|
|
219
|
-
'display_name': 'CloudFront',
|
|
220
|
-
'is_vpc': False,
|
|
221
|
-
},
|
|
222
|
-
# Bedrock
|
|
223
|
-
'bedrock': {
|
|
224
|
-
'primary': ['aws_bedrockagent_knowledge_base'],
|
|
225
|
-
'aggregate': [],
|
|
226
|
-
'icon': 'aws_bedrockagent_knowledge_base',
|
|
227
|
-
'display_name': 'Bedrock KB',
|
|
228
|
-
'is_vpc': False,
|
|
229
|
-
},
|
|
230
|
-
# Budgets
|
|
231
|
-
'budgets': {
|
|
232
|
-
'primary': ['aws_budgets_budget'],
|
|
233
|
-
'aggregate': [],
|
|
234
|
-
'icon': 'aws_budgets_budget',
|
|
235
|
-
'display_name': 'Budgets',
|
|
236
|
-
'is_vpc': False,
|
|
237
|
-
},
|
|
238
|
-
# EC2 (standalone instances like DevOps agent)
|
|
239
|
-
'ec2': {
|
|
240
|
-
'primary': ['aws_instance'],
|
|
241
|
-
'aggregate': ['aws_launch_template'],
|
|
242
|
-
'icon': 'aws_instance',
|
|
243
|
-
'display_name': 'EC2',
|
|
244
|
-
'is_vpc': True,
|
|
245
|
-
},
|
|
246
|
-
# MongoDB Atlas (external)
|
|
247
|
-
'mongodb': {
|
|
248
|
-
'primary': ['mongodbatlas_cluster'],
|
|
249
|
-
'aggregate': ['mongodbatlas_network_peering', 'mongodbatlas_project_ip_access_list'],
|
|
250
|
-
'icon': 'aws_dynamodb_table', # Use DynamoDB icon as fallback
|
|
251
|
-
'display_name': 'MongoDB Atlas',
|
|
252
|
-
'is_vpc': False,
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
# High-level connections between service types
|
|
257
|
-
LOGICAL_CONNECTIONS = [
|
|
258
|
-
# Internet -> WAF -> CloudFront -> ALB
|
|
259
|
-
('cloudfront', 'alb', 'HTTPS', 'data_flow'),
|
|
260
|
-
('waf', 'alb', 'protects', 'default'),
|
|
261
|
-
('waf', 'cognito', 'protects', 'default'),
|
|
262
|
-
|
|
263
|
-
# ALB -> ECS
|
|
264
|
-
('alb', 'ecs', 'routes to', 'data_flow'),
|
|
265
|
-
|
|
266
|
-
# ECS -> various services
|
|
267
|
-
('ecs', 'sqs', 'sends/receives', 'data_flow'),
|
|
268
|
-
('ecs', 's3', 'reads/writes', 'data_flow'),
|
|
269
|
-
('ecs', 'dynamodb', 'queries', 'data_flow'),
|
|
270
|
-
('ecs', 'secrets', 'reads', 'default'),
|
|
271
|
-
('ecs', 'bedrock', 'invokes', 'data_flow'),
|
|
272
|
-
|
|
273
|
-
# S3 -> SQS (notifications)
|
|
274
|
-
('s3', 'sqs', 'triggers', 'trigger'),
|
|
275
|
-
|
|
276
|
-
# SNS for alerts
|
|
277
|
-
('cloudwatch', 'sns', 'alerts', 'trigger'),
|
|
278
|
-
('sqs', 'sns', 'DLQ alerts', 'trigger'),
|
|
279
|
-
|
|
280
|
-
# Encryption
|
|
281
|
-
('kms', 's3', 'encrypts', 'encrypt'),
|
|
282
|
-
('kms', 'sqs', 'encrypts', 'encrypt'),
|
|
283
|
-
('kms', 'sns', 'encrypts', 'encrypt'),
|
|
284
|
-
('kms', 'secrets', 'encrypts', 'encrypt'),
|
|
285
|
-
|
|
286
|
-
# DNS
|
|
287
|
-
('route53', 'alb', 'resolves', 'default'),
|
|
288
|
-
('route53', 'cloudfront', 'resolves', 'default'),
|
|
289
|
-
|
|
290
|
-
# Certificates
|
|
291
|
-
('acm', 'alb', 'TLS', 'default'),
|
|
292
|
-
('acm', 'cloudfront', 'TLS', 'default'),
|
|
293
|
-
|
|
294
|
-
# Cognito auth
|
|
295
|
-
('cognito', 'alb', 'authenticates', 'default'),
|
|
296
|
-
|
|
297
|
-
# ECR -> ECS
|
|
298
|
-
('ecr', 'ecs', 'images', 'data_flow'),
|
|
299
|
-
|
|
300
|
-
# External
|
|
301
|
-
('ecs', 'mongodb', 'queries', 'data_flow'),
|
|
302
|
-
]
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class ResourceAggregator:
|
|
306
|
-
"""Aggregates Terraform resources into logical services."""
|
|
307
|
-
|
|
308
|
-
def __init__(self, config_loader: Optional[ConfigLoader] = None):
|
|
309
|
-
self._config = config_loader or ConfigLoader()
|
|
310
|
-
self._aggregation_rules = self._build_aggregation_rules()
|
|
311
|
-
self._logical_connections = self._config.get_logical_connections()
|
|
312
|
-
self._build_type_to_rule_map()
|
|
313
|
-
|
|
314
|
-
def _build_aggregation_rules(self) -> Dict[str, Dict[str, Any]]:
|
|
315
|
-
"""Build aggregation rules dict from config."""
|
|
316
|
-
flat_rules = self._config.get_flat_aggregation_rules()
|
|
317
|
-
result = {}
|
|
318
|
-
for service_name, config in flat_rules.items():
|
|
319
|
-
# Map YAML format (primary/secondary/in_vpc) to internal format
|
|
320
|
-
result[service_name] = {
|
|
321
|
-
'primary': config.get("primary", []),
|
|
322
|
-
'aggregate': config.get("secondary", []), # secondary in YAML -> aggregate internally
|
|
323
|
-
'icon': config.get("primary", [""])[0] if config.get("primary") else "",
|
|
324
|
-
'display_name': service_name.replace("_", " ").title(),
|
|
325
|
-
'is_vpc': config.get("in_vpc", False),
|
|
326
|
-
}
|
|
327
|
-
return result
|
|
328
|
-
|
|
329
|
-
def _build_type_to_rule_map(self) -> None:
|
|
330
|
-
"""Build a mapping from resource type to aggregation rule."""
|
|
331
|
-
self._type_to_rule: Dict[str, str] = {}
|
|
332
|
-
for rule_name, rule in self._aggregation_rules.items():
|
|
333
|
-
for res_type in rule['primary']:
|
|
334
|
-
self._type_to_rule[res_type] = rule_name
|
|
335
|
-
for res_type in rule['aggregate']:
|
|
336
|
-
self._type_to_rule[res_type] = rule_name
|
|
337
|
-
|
|
338
|
-
def aggregate(self, parse_result: ParseResult) -> AggregatedResult:
|
|
339
|
-
"""Aggregate parsed resources into logical services."""
|
|
340
|
-
result = AggregatedResult()
|
|
341
|
-
|
|
342
|
-
# Group resources by aggregation rule
|
|
343
|
-
rule_resources: Dict[str, List[TerraformResource]] = {}
|
|
344
|
-
unmatched: List[TerraformResource] = []
|
|
345
|
-
|
|
346
|
-
for resource in parse_result.resources:
|
|
347
|
-
rule_name = self._type_to_rule.get(resource.resource_type)
|
|
348
|
-
if rule_name:
|
|
349
|
-
rule_resources.setdefault(rule_name, []).append(resource)
|
|
350
|
-
else:
|
|
351
|
-
unmatched.append(resource)
|
|
352
|
-
|
|
353
|
-
# Create logical services from grouped resources
|
|
354
|
-
for rule_name, resources in rule_resources.items():
|
|
355
|
-
rule = self._aggregation_rules[rule_name]
|
|
356
|
-
|
|
357
|
-
# Count primary resources
|
|
358
|
-
primary_count = sum(1 for r in resources if r.resource_type in rule['primary'])
|
|
359
|
-
if primary_count == 0:
|
|
360
|
-
continue # Skip if no primary resources
|
|
361
|
-
|
|
362
|
-
service = LogicalService(
|
|
363
|
-
service_type=rule_name,
|
|
364
|
-
name=rule['display_name'],
|
|
365
|
-
icon_resource_type=rule['icon'],
|
|
366
|
-
resources=resources,
|
|
367
|
-
count=primary_count,
|
|
368
|
-
is_vpc_resource=rule['is_vpc'],
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
result.services.append(service)
|
|
372
|
-
if service.is_vpc_resource:
|
|
373
|
-
result.vpc_services.append(service)
|
|
374
|
-
else:
|
|
375
|
-
result.global_services.append(service)
|
|
376
|
-
|
|
377
|
-
# Create logical connections based on which services exist
|
|
378
|
-
existing_services = {s.service_type for s in result.services}
|
|
379
|
-
for conn in self._logical_connections:
|
|
380
|
-
source = conn.get("source", "")
|
|
381
|
-
target = conn.get("target", "")
|
|
382
|
-
if source in existing_services and target in existing_services:
|
|
383
|
-
result.connections.append(LogicalConnection(
|
|
384
|
-
source_id=f"{source}.{self._aggregation_rules[source]['display_name']}",
|
|
385
|
-
target_id=f"{target}.{self._aggregation_rules[target]['display_name']}",
|
|
386
|
-
label=conn.get("label", ""),
|
|
387
|
-
connection_type=conn.get("type", "default"),
|
|
388
|
-
))
|
|
389
|
-
|
|
390
|
-
return result
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def aggregate_resources(parse_result: ParseResult) -> AggregatedResult:
|
|
394
|
-
"""Convenience function to aggregate resources."""
|
|
395
|
-
aggregator = ResourceAggregator()
|
|
396
|
-
return aggregator.aggregate(parse_result)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{terraformgraph-1.0.1 → terraformgraph-1.0.3}/terraformgraph/config/logical_connections.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|