terraformgraph 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
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/__init__.py +1 -1
- terraformgraph/__main__.py +1 -1
- terraformgraph/aggregator.py +941 -300
- terraformgraph/config/aggregation_rules.yaml +276 -1
- terraformgraph/config_loader.py +9 -8
- terraformgraph/icons.py +504 -521
- terraformgraph/layout.py +580 -116
- terraformgraph/main.py +251 -48
- terraformgraph/parser.py +328 -86
- terraformgraph/renderer.py +1887 -170
- terraformgraph/terraform_tools.py +355 -0
- terraformgraph/variable_resolver.py +180 -0
- terraformgraph-1.0.4.dist-info/METADATA +386 -0
- terraformgraph-1.0.4.dist-info/RECORD +19 -0
- {terraformgraph-1.0.2.dist-info → terraformgraph-1.0.4.dist-info}/licenses/LICENSE +1 -1
- terraformgraph-1.0.2.dist-info/METADATA +0 -163
- terraformgraph-1.0.2.dist-info/RECORD +0 -17
- {terraformgraph-1.0.2.dist-info → terraformgraph-1.0.4.dist-info}/WHEEL +0 -0
- {terraformgraph-1.0.2.dist-info → terraformgraph-1.0.4.dist-info}/entry_points.txt +0 -0
- {terraformgraph-1.0.2.dist-info → terraformgraph-1.0.4.dist-info}/top_level.txt +0 -0
terraformgraph/main.py
CHANGED
|
@@ -5,79 +5,231 @@ terraformgraph - Terraform Diagram Generator
|
|
|
5
5
|
Generates AWS infrastructure diagrams from Terraform code using official AWS icons.
|
|
6
6
|
Creates high-level architectural diagrams with logical service groupings.
|
|
7
7
|
|
|
8
|
+
PREREQUISITES:
|
|
9
|
+
Before using terraformgraph, run these commands in your Terraform directory:
|
|
10
|
+
|
|
11
|
+
cd ./infrastructure
|
|
12
|
+
terraform init
|
|
13
|
+
terraform apply # or terraform plan for undeployed infrastructure
|
|
14
|
+
|
|
8
15
|
Usage:
|
|
9
|
-
#
|
|
16
|
+
# Generate diagram (auto-generates state JSON if needed)
|
|
10
17
|
terraformgraph -t ./infrastructure
|
|
11
18
|
|
|
12
|
-
#
|
|
13
|
-
terraformgraph -t ./infrastructure -
|
|
19
|
+
# With pre-generated state file
|
|
20
|
+
terraformgraph -t ./infrastructure --state-file state.json
|
|
21
|
+
|
|
22
|
+
# With specific environment subdirectory
|
|
23
|
+
terraformgraph -t ./infrastructure -e prod
|
|
14
24
|
|
|
15
25
|
# With custom output path
|
|
16
26
|
terraformgraph -t ./infrastructure -o my-diagram.html
|
|
17
|
-
|
|
18
|
-
# With custom icons path
|
|
19
|
-
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
20
27
|
"""
|
|
21
28
|
|
|
22
29
|
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import subprocess
|
|
23
32
|
import sys
|
|
24
33
|
from pathlib import Path
|
|
34
|
+
from typing import Optional
|
|
25
35
|
|
|
26
|
-
from .aggregator import
|
|
36
|
+
from .aggregator import ResourceAggregator
|
|
27
37
|
from .icons import IconMapper
|
|
28
38
|
from .layout import LayoutConfig, LayoutEngine
|
|
29
39
|
from .parser import TerraformParser
|
|
30
40
|
from .renderer import HTMLRenderer, SVGRenderer
|
|
41
|
+
from .terraform_tools import TerraformToolsRunner
|
|
42
|
+
|
|
43
|
+
# File name for auto-generated state cache
|
|
44
|
+
STATE_CACHE_FILE = ".terraformgraph-state.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _generate_state_json(terraform_dir: Path, verbose: bool = False) -> Path:
|
|
48
|
+
"""Generate state JSON from terraform show -json.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
terraform_dir: Path to terraform directory
|
|
52
|
+
verbose: Print progress messages
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to the generated JSON file
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
RuntimeError: If terraform is not available or not initialized
|
|
59
|
+
"""
|
|
60
|
+
runner = TerraformToolsRunner(terraform_dir)
|
|
61
|
+
|
|
62
|
+
if not runner.check_terraform_available():
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
"Terraform CLI not found in PATH.\n"
|
|
65
|
+
"Please install Terraform: https://developer.hashicorp.com/terraform/install"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not runner.check_initialized():
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
f"Terraform not initialized in {terraform_dir}.\n"
|
|
71
|
+
f"Please run: cd {terraform_dir} && terraform init"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if verbose:
|
|
75
|
+
print("Generating state JSON from terraform show -json...")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["terraform", "show", "-json"],
|
|
80
|
+
cwd=terraform_dir,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
timeout=120,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if result.returncode != 0:
|
|
87
|
+
raise RuntimeError(f"terraform show -json failed: {result.stderr}")
|
|
88
|
+
|
|
89
|
+
if not result.stdout.strip():
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
"No terraform state found.\n"
|
|
92
|
+
f"Please run: cd {terraform_dir} && terraform apply (or terraform plan)"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Validate JSON
|
|
96
|
+
try:
|
|
97
|
+
json_data = json.loads(result.stdout)
|
|
98
|
+
except json.JSONDecodeError as e:
|
|
99
|
+
raise RuntimeError(f"Invalid JSON from terraform show: {e}")
|
|
100
|
+
|
|
101
|
+
# Save to cache file
|
|
102
|
+
cache_file = terraform_dir / STATE_CACHE_FILE
|
|
103
|
+
with open(cache_file, "w", encoding="utf-8") as f:
|
|
104
|
+
json.dump(json_data, f, indent=2)
|
|
105
|
+
|
|
106
|
+
if verbose:
|
|
107
|
+
print(f"State cached to {cache_file}")
|
|
108
|
+
|
|
109
|
+
return cache_file
|
|
110
|
+
|
|
111
|
+
except subprocess.TimeoutExpired:
|
|
112
|
+
raise RuntimeError("terraform show -json timed out after 120 seconds")
|
|
113
|
+
except OSError as e:
|
|
114
|
+
raise RuntimeError(f"Error running terraform: {e}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_state_file(
|
|
118
|
+
terraform_dir: Path, state_file_arg: Optional[str], verbose: bool = False
|
|
119
|
+
) -> Path:
|
|
120
|
+
"""Determine which state file to use.
|
|
121
|
+
|
|
122
|
+
Priority:
|
|
123
|
+
1. User-specified --state-file argument
|
|
124
|
+
2. Existing .terraformgraph-state.json cache
|
|
125
|
+
3. Existing plan.json or state.json in terraform dir
|
|
126
|
+
4. Auto-generate from terraform show -json
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
terraform_dir: Path to terraform directory
|
|
130
|
+
state_file_arg: User-provided state file path (or None)
|
|
131
|
+
verbose: Print progress messages
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Path to the state file to use
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
RuntimeError: If state cannot be obtained
|
|
138
|
+
"""
|
|
139
|
+
# 1. User-specified file
|
|
140
|
+
if state_file_arg:
|
|
141
|
+
state_path = Path(state_file_arg)
|
|
142
|
+
if not state_path.exists():
|
|
143
|
+
raise RuntimeError(f"State file not found: {state_path}")
|
|
144
|
+
if verbose:
|
|
145
|
+
print(f"Using specified state file: {state_path}")
|
|
146
|
+
return state_path
|
|
147
|
+
|
|
148
|
+
# 2. Check for cached state file
|
|
149
|
+
cache_file = terraform_dir / STATE_CACHE_FILE
|
|
150
|
+
if cache_file.exists():
|
|
151
|
+
if verbose:
|
|
152
|
+
print(f"Using cached state: {cache_file}")
|
|
153
|
+
return cache_file
|
|
154
|
+
|
|
155
|
+
# 3. Check for existing JSON files in terraform dir
|
|
156
|
+
for filename in ["plan.json", "state.json", "terraform.tfstate.json"]:
|
|
157
|
+
json_file = terraform_dir / filename
|
|
158
|
+
if json_file.exists():
|
|
159
|
+
if verbose:
|
|
160
|
+
print(f"Using existing state file: {json_file}")
|
|
161
|
+
return json_file
|
|
162
|
+
|
|
163
|
+
# 4. Auto-generate from terraform
|
|
164
|
+
return _generate_state_json(terraform_dir, verbose)
|
|
31
165
|
|
|
32
166
|
|
|
33
167
|
def main():
|
|
34
168
|
parser = argparse.ArgumentParser(
|
|
35
|
-
description=
|
|
169
|
+
description="Generate AWS infrastructure diagrams from Terraform code.",
|
|
36
170
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
37
|
-
epilog=
|
|
171
|
+
epilog="""
|
|
172
|
+
Prerequisites:
|
|
173
|
+
Before running terraformgraph, ensure your Terraform is initialized:
|
|
174
|
+
|
|
175
|
+
cd ./infrastructure
|
|
176
|
+
terraform init
|
|
177
|
+
terraform apply # or terraform plan
|
|
178
|
+
|
|
38
179
|
Examples:
|
|
39
|
-
#
|
|
180
|
+
# Generate diagram (auto-generates state if needed)
|
|
40
181
|
terraformgraph -t ./infrastructure
|
|
41
182
|
|
|
42
|
-
#
|
|
43
|
-
|
|
183
|
+
# With pre-generated state file
|
|
184
|
+
terraform show -json > state.json
|
|
185
|
+
terraformgraph -t ./infrastructure --state-file state.json
|
|
44
186
|
|
|
45
|
-
# With
|
|
46
|
-
terraformgraph -t ./infrastructure -
|
|
187
|
+
# With specific environment subdirectory
|
|
188
|
+
terraformgraph -t ./infrastructure -e prod
|
|
47
189
|
|
|
48
190
|
# With custom icons path
|
|
49
|
-
terraformgraph -t ./infrastructure -i /path/to/icons
|
|
50
|
-
|
|
191
|
+
terraformgraph -t ./infrastructure -i /path/to/aws-icons
|
|
192
|
+
""",
|
|
51
193
|
)
|
|
52
194
|
|
|
53
195
|
parser.add_argument(
|
|
54
|
-
|
|
55
|
-
required=True,
|
|
56
|
-
help='Path to the Terraform infrastructure directory'
|
|
196
|
+
"-t", "--terraform", required=True, help="Path to the Terraform infrastructure directory"
|
|
57
197
|
)
|
|
58
198
|
|
|
59
199
|
parser.add_argument(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
200
|
+
"-e",
|
|
201
|
+
"--environment",
|
|
202
|
+
help="Environment name (dev, staging, prod). If not provided, parses the terraform directory directly.",
|
|
203
|
+
default=None,
|
|
63
204
|
)
|
|
64
205
|
|
|
65
206
|
parser.add_argument(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
207
|
+
"-i",
|
|
208
|
+
"--icons",
|
|
209
|
+
help="Path to AWS icons directory (auto-discovers in ./aws-official-icons, ~/aws-official-icons, ~/.terraformgraph/icons)",
|
|
210
|
+
default=None,
|
|
69
211
|
)
|
|
70
212
|
|
|
71
213
|
parser.add_argument(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
214
|
+
"-o",
|
|
215
|
+
"--output",
|
|
216
|
+
default="terraformgraph.html",
|
|
217
|
+
help="Output file path (HTML). Default: terraformgraph.html",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
|
|
221
|
+
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"--state-file",
|
|
224
|
+
"-s",
|
|
225
|
+
metavar="FILE",
|
|
226
|
+
help="Path to terraform state JSON file (from 'terraform show -json'). If not provided, auto-generates from terraform CLI.",
|
|
75
227
|
)
|
|
76
228
|
|
|
77
229
|
parser.add_argument(
|
|
78
|
-
|
|
79
|
-
action=
|
|
80
|
-
help=
|
|
230
|
+
"--refresh-state",
|
|
231
|
+
action="store_true",
|
|
232
|
+
help="Force regeneration of state JSON even if cached file exists.",
|
|
81
233
|
)
|
|
82
234
|
|
|
83
235
|
args = parser.parse_args()
|
|
@@ -108,7 +260,9 @@ Examples:
|
|
|
108
260
|
break
|
|
109
261
|
|
|
110
262
|
if icons_path and not icons_path.exists():
|
|
111
|
-
print(
|
|
263
|
+
print(
|
|
264
|
+
f"Warning: Icons path not found: {icons_path}. Using fallback colors.", file=sys.stderr
|
|
265
|
+
)
|
|
112
266
|
icons_path = None
|
|
113
267
|
elif icons_path and args.verbose:
|
|
114
268
|
print(f"Using icons from: {icons_path}")
|
|
@@ -120,7 +274,11 @@ Examples:
|
|
|
120
274
|
title = f"{args.environment.upper()} Environment"
|
|
121
275
|
if not parse_path.exists():
|
|
122
276
|
print(f"Error: Environment not found: {parse_path}", file=sys.stderr)
|
|
123
|
-
available = [
|
|
277
|
+
available = [
|
|
278
|
+
d.name
|
|
279
|
+
for d in terraform_path.iterdir()
|
|
280
|
+
if d.is_dir() and not d.name.startswith(".")
|
|
281
|
+
]
|
|
124
282
|
print(f"Available environments: {available}", file=sys.stderr)
|
|
125
283
|
sys.exit(1)
|
|
126
284
|
else:
|
|
@@ -129,11 +287,26 @@ Examples:
|
|
|
129
287
|
title = terraform_path.name
|
|
130
288
|
|
|
131
289
|
try:
|
|
290
|
+
# Delete cached state if --refresh-state is specified
|
|
291
|
+
if args.refresh_state:
|
|
292
|
+
cache_file = parse_path / STATE_CACHE_FILE
|
|
293
|
+
if cache_file.exists():
|
|
294
|
+
cache_file.unlink()
|
|
295
|
+
if args.verbose:
|
|
296
|
+
print(f"Removed cached state: {cache_file}")
|
|
297
|
+
|
|
298
|
+
# Get state file (auto-generates if needed)
|
|
299
|
+
state_file = _get_state_file(parse_path, args.state_file, args.verbose)
|
|
300
|
+
|
|
132
301
|
# Parse Terraform files
|
|
133
302
|
if args.verbose:
|
|
134
303
|
print(f"Parsing Terraform files from {parse_path}...")
|
|
135
304
|
|
|
136
|
-
tf_parser = TerraformParser(
|
|
305
|
+
tf_parser = TerraformParser(
|
|
306
|
+
str(terraform_path),
|
|
307
|
+
use_terraform_state=True,
|
|
308
|
+
state_file=str(state_file),
|
|
309
|
+
)
|
|
137
310
|
|
|
138
311
|
if args.environment:
|
|
139
312
|
parse_result = tf_parser.parse_environment(args.environment)
|
|
@@ -143,44 +316,70 @@ Examples:
|
|
|
143
316
|
if args.verbose:
|
|
144
317
|
print(f"Found {len(parse_result.resources)} raw resources")
|
|
145
318
|
print(f"Found {len(parse_result.modules)} module calls")
|
|
319
|
+
if tf_parser.get_state_result():
|
|
320
|
+
state = tf_parser.get_state_result()
|
|
321
|
+
print(f"Enhanced with terraform state: {len(state.resources)} resources")
|
|
146
322
|
|
|
147
323
|
# Aggregate into logical services
|
|
148
324
|
if args.verbose:
|
|
149
325
|
print("Aggregating into logical services...")
|
|
150
326
|
|
|
151
|
-
|
|
327
|
+
aggregator = ResourceAggregator()
|
|
328
|
+
aggregated = aggregator.aggregate(
|
|
329
|
+
parse_result,
|
|
330
|
+
terraform_dir=parse_path,
|
|
331
|
+
state_result=tf_parser.get_state_result(),
|
|
332
|
+
)
|
|
152
333
|
|
|
153
334
|
if args.verbose:
|
|
154
335
|
print(f"Created {len(aggregated.services)} logical services:")
|
|
155
336
|
for service in aggregated.services:
|
|
156
|
-
print(
|
|
337
|
+
print(
|
|
338
|
+
f" - {service.name}: {len(service.resources)} resources (count: {service.count})"
|
|
339
|
+
)
|
|
157
340
|
print(f"Created {len(aggregated.connections)} logical connections")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
341
|
+
if aggregated.vpc_structure:
|
|
342
|
+
vpc = aggregated.vpc_structure
|
|
343
|
+
print(f"VPC Structure: {vpc.name}")
|
|
344
|
+
print(f" - {len(vpc.availability_zones)} Availability Zones")
|
|
345
|
+
for az in vpc.availability_zones:
|
|
346
|
+
print(f" - {az.name}: {len(az.subnets)} subnets")
|
|
347
|
+
print(f" - {len(vpc.endpoints)} VPC Endpoints")
|
|
348
|
+
|
|
349
|
+
# Setup layout (config is now responsive and scaled based on content)
|
|
350
|
+
base_config = LayoutConfig()
|
|
351
|
+
layout_engine = LayoutEngine(base_config)
|
|
352
|
+
positions, groups, actual_height = layout_engine.compute_layout(aggregated)
|
|
353
|
+
|
|
354
|
+
# Get the scaled config from the layout engine
|
|
355
|
+
responsive_config = layout_engine.config
|
|
163
356
|
|
|
164
357
|
if args.verbose:
|
|
165
358
|
print(f"Positioned {len(positions)} services")
|
|
359
|
+
print(
|
|
360
|
+
f"Canvas: {responsive_config.canvas_width}x{actual_height} (scale: {layout_engine._compute_responsive_scale(aggregated):.2f})"
|
|
361
|
+
)
|
|
166
362
|
|
|
167
|
-
# Setup renderers
|
|
363
|
+
# Setup renderers with the responsive config
|
|
168
364
|
icon_mapper = IconMapper(str(icons_path) if icons_path else None)
|
|
169
|
-
svg_renderer = SVGRenderer(icon_mapper,
|
|
365
|
+
svg_renderer = SVGRenderer(icon_mapper, responsive_config)
|
|
170
366
|
html_renderer = HTMLRenderer(svg_renderer)
|
|
171
367
|
|
|
172
|
-
# Generate HTML
|
|
368
|
+
# Generate HTML with actual calculated height
|
|
173
369
|
if args.verbose:
|
|
174
370
|
print("Generating HTML output...")
|
|
175
371
|
|
|
176
372
|
html_content = html_renderer.render_html(
|
|
177
|
-
aggregated,
|
|
178
|
-
|
|
373
|
+
aggregated,
|
|
374
|
+
positions,
|
|
375
|
+
groups,
|
|
376
|
+
environment=args.environment or title,
|
|
377
|
+
actual_height=actual_height,
|
|
179
378
|
)
|
|
180
379
|
|
|
181
380
|
# Write output
|
|
182
381
|
output_path = Path(args.output)
|
|
183
|
-
output_path.write_text(html_content, encoding=
|
|
382
|
+
output_path.write_text(html_content, encoding="utf-8")
|
|
184
383
|
|
|
185
384
|
print(f"Diagram generated: {output_path.absolute()}")
|
|
186
385
|
print("\nSummary:")
|
|
@@ -188,13 +387,17 @@ Examples:
|
|
|
188
387
|
print(f" Resources: {sum(len(s.resources) for s in aggregated.services)}")
|
|
189
388
|
print(f" Connections: {len(aggregated.connections)}")
|
|
190
389
|
|
|
390
|
+
except RuntimeError as e:
|
|
391
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
392
|
+
sys.exit(1)
|
|
191
393
|
except Exception as e:
|
|
192
394
|
print(f"Error: {e}", file=sys.stderr)
|
|
193
395
|
if args.verbose:
|
|
194
396
|
import traceback
|
|
397
|
+
|
|
195
398
|
traceback.print_exc()
|
|
196
399
|
sys.exit(1)
|
|
197
400
|
|
|
198
401
|
|
|
199
|
-
if __name__ ==
|
|
402
|
+
if __name__ == "__main__":
|
|
200
403
|
main()
|