tfplan-report 0.1.0__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.
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tfplan-report
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate Markdown reports from Terraform plans
|
|
5
|
+
Author: Aswin
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Aswin-00/tfreadme.git
|
|
8
|
+
Keywords: terraform,terraform-plan,iac,devops,aws,cloud,markdown,report,automation,cli
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: tabulate>=0.9.0
|
|
27
|
+
|
|
28
|
+
# Terraform Plan Report Generator
|
|
29
|
+
|
|
30
|
+
Generate clean, human-readable Markdown reports from Terraform plan files.
|
|
31
|
+
|
|
32
|
+
This utility converts the output of `terraform show -json` into a structured Markdown report that is easier to review during infrastructure changes, pull requests, and CI/CD pipelines.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
* Generate Markdown reports from Terraform plan files
|
|
37
|
+
* Summary of resource actions
|
|
38
|
+
|
|
39
|
+
* CREATE
|
|
40
|
+
* UPDATE
|
|
41
|
+
* DELETE
|
|
42
|
+
* REPLACE
|
|
43
|
+
* Resource type summary
|
|
44
|
+
* Friendly resource name detection
|
|
45
|
+
* Resource ID extraction
|
|
46
|
+
* Terraform resource address listing
|
|
47
|
+
* Collapsible sections for improved readability
|
|
48
|
+
* Lightweight and dependency-minimal
|
|
49
|
+
|
|
50
|
+
## Example Workflow
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
terraform plan -out=tfplan
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
terraform show -json tfplan
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
Terraform Plan Report
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
README_TFPLAN.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
Clone the repository:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/<your-username>/tfplan-report.git
|
|
71
|
+
cd tfplan-report
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Install the package:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or install the dependency manually:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install tabulate
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
* Python 3.9+
|
|
89
|
+
* Terraform CLI
|
|
90
|
+
* tabulate
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
Generate a Terraform plan:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
terraform plan -out=tfplan
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Generate the Markdown report:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
tfreadme tfplan
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
or
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python tfreadme.py tfplan
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The tool generates:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
README_TFPLAN.md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Sample Output
|
|
119
|
+
|
|
120
|
+
### Plan Summary
|
|
121
|
+
|
|
122
|
+
| Action | Count |
|
|
123
|
+
| ------- | ----: |
|
|
124
|
+
| CREATE | 12 |
|
|
125
|
+
| UPDATE | 5 |
|
|
126
|
+
| DELETE | 2 |
|
|
127
|
+
| REPLACE | 1 |
|
|
128
|
+
|
|
129
|
+
### Resource Type Summary
|
|
130
|
+
|
|
131
|
+
| Resource Type | Count |
|
|
132
|
+
| ------------------ | ----: |
|
|
133
|
+
| aws_instance | 8 |
|
|
134
|
+
| aws_security_group | 4 |
|
|
135
|
+
| aws_iam_role | 2 |
|
|
136
|
+
|
|
137
|
+
### CREATE
|
|
138
|
+
|
|
139
|
+
| Name | Type | Resource ID | Terraform Address |
|
|
140
|
+
| ---------- | ------------ | ----------- | ----------------- |
|
|
141
|
+
| web-server | aws_instance | N/A | aws_instance.web |
|
|
142
|
+
|
|
143
|
+
## Project Structure
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
.
|
|
147
|
+
├── pyproject.toml
|
|
148
|
+
├── requirements.txt
|
|
149
|
+
├── tfreadme.py
|
|
150
|
+
└── README.md
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## How It Works
|
|
154
|
+
|
|
155
|
+
The tool:
|
|
156
|
+
|
|
157
|
+
1. Reads a Terraform binary plan file
|
|
158
|
+
2. Executes `terraform show -json`
|
|
159
|
+
3. Parses the JSON output
|
|
160
|
+
4. Classifies resource actions
|
|
161
|
+
5. Groups resources by operation
|
|
162
|
+
6. Generates a clean Markdown report
|
|
163
|
+
|
|
164
|
+
## Current Capabilities
|
|
165
|
+
|
|
166
|
+
* Parse Terraform plan files
|
|
167
|
+
* Generate Markdown summaries
|
|
168
|
+
* Count resources by action
|
|
169
|
+
* Count resources by type
|
|
170
|
+
* Extract resource names
|
|
171
|
+
* Extract resource IDs
|
|
172
|
+
* Display Terraform addresses
|
|
173
|
+
* Produce review-friendly output
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## Technologies Used
|
|
177
|
+
|
|
178
|
+
* Python
|
|
179
|
+
* Terraform
|
|
180
|
+
* JSON
|
|
181
|
+
* tabulate
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT License
|
|
187
|
+
|
|
188
|
+
## Author
|
|
189
|
+
|
|
190
|
+
Developed by **Aswin** as a practical DevOps automation tool to simplify Terraform plan reviews and improve infrastructure change visibility.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
tfreadme.py,sha256=1gv36a6AhOLx7HWcH55vcpSateO3suYrCDJtCbXeD5c,5585
|
|
2
|
+
tfplan_report-0.1.0.dist-info/METADATA,sha256=TK9o0bPE0x_Do2HeLk44w0B-BCdoy3xWCkMY6bil9Nw,3931
|
|
3
|
+
tfplan_report-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
tfplan_report-0.1.0.dist-info/entry_points.txt,sha256=Ob_yWc8oWMYHxl0HJCZZnfRbPHikQBb2pSKN5EXHgA8,43
|
|
5
|
+
tfplan_report-0.1.0.dist-info/top_level.txt,sha256=mBhBoR1u_S4yL4XdRVnvWgx9RpUYYFKOrFRgzGsw7o0,9
|
|
6
|
+
tfplan_report-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tfreadme
|
tfreadme.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from tabulate import tabulate
|
|
7
|
+
import subprocess
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
OUTPUT_FILE = "README_TFPLAN.md"
|
|
12
|
+
def get_resource_name(change):
|
|
13
|
+
"""
|
|
14
|
+
Extract a friendly name from before/after state.
|
|
15
|
+
"""
|
|
16
|
+
candidates = [
|
|
17
|
+
change.get("after", {}),
|
|
18
|
+
change.get("before", {})
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for obj in candidates:
|
|
22
|
+
if not isinstance(obj, dict):
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if obj.get("name"):
|
|
26
|
+
return str(obj["name"])
|
|
27
|
+
|
|
28
|
+
tags = obj.get("tags", {})
|
|
29
|
+
if isinstance(tags, dict) and tags.get("Name"):
|
|
30
|
+
return str(tags["Name"])
|
|
31
|
+
|
|
32
|
+
tags_all = obj.get("tags_all", {})
|
|
33
|
+
if isinstance(tags_all, dict) and tags_all.get("Name"):
|
|
34
|
+
return str(tags_all["Name"])
|
|
35
|
+
|
|
36
|
+
return "N/A"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_resource_id(change: dict) -> str:
|
|
40
|
+
for obj in (
|
|
41
|
+
change.get("before"),
|
|
42
|
+
change.get("after"),
|
|
43
|
+
):
|
|
44
|
+
if isinstance(obj, dict):
|
|
45
|
+
if obj.get("id"):
|
|
46
|
+
return str(obj["id"])
|
|
47
|
+
|
|
48
|
+
return "N/A"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def classify_action(actions):
|
|
52
|
+
actions = set(actions)
|
|
53
|
+
|
|
54
|
+
if actions == {"create"}:
|
|
55
|
+
return "CREATE"
|
|
56
|
+
|
|
57
|
+
if actions == {"update"}:
|
|
58
|
+
return "UPDATE"
|
|
59
|
+
|
|
60
|
+
if actions == {"delete"}:
|
|
61
|
+
return "DELETE"
|
|
62
|
+
|
|
63
|
+
if actions == {"create", "delete"}:
|
|
64
|
+
return "REPLACE"
|
|
65
|
+
|
|
66
|
+
return ",".join(sorted(actions)).upper()
|
|
67
|
+
|
|
68
|
+
def load_plan(plan_file: str) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Load a Terraform binary plan using `terraform show -json`
|
|
71
|
+
and return it as a Python dictionary.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["terraform", "show", "-json", plan_file],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
check=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return json.loads(result.stdout)
|
|
82
|
+
|
|
83
|
+
except subprocess.CalledProcessError as e:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"terraform show failed:\n{e.stderr}"
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
except json.JSONDecodeError as e:
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
"Failed to parse terraform JSON output."
|
|
91
|
+
) from e
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_report(plan):
|
|
95
|
+
grouped = defaultdict(list)
|
|
96
|
+
summary = defaultdict(int)
|
|
97
|
+
type_summary = defaultdict(int)
|
|
98
|
+
|
|
99
|
+
resource_changes = plan.get("resource_changes", [])
|
|
100
|
+
|
|
101
|
+
for rc in resource_changes:
|
|
102
|
+
change = rc.get("change", {})
|
|
103
|
+
actions = change.get("actions", [])
|
|
104
|
+
|
|
105
|
+
action = classify_action(actions)
|
|
106
|
+
|
|
107
|
+
resource = {
|
|
108
|
+
"name": get_resource_name(change),
|
|
109
|
+
"type": rc.get("type", "N/A"),
|
|
110
|
+
"address": rc.get("address", "N/A"),
|
|
111
|
+
"id": get_resource_id(change),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
grouped[action].append(resource)
|
|
115
|
+
|
|
116
|
+
summary[action] += 1
|
|
117
|
+
type_summary[resource["type"]] += 1
|
|
118
|
+
|
|
119
|
+
md = []
|
|
120
|
+
|
|
121
|
+
md.append("# Terraform Plan Report")
|
|
122
|
+
md.append("")
|
|
123
|
+
md.append("Generated from `terraform show -json` output.")
|
|
124
|
+
md.append("")
|
|
125
|
+
|
|
126
|
+
# Summary
|
|
127
|
+
md.append("## Plan Summary")
|
|
128
|
+
md.append("")
|
|
129
|
+
|
|
130
|
+
summary_rows = [
|
|
131
|
+
[action, count]
|
|
132
|
+
for action, count in sorted(summary.items())
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
md.append(
|
|
136
|
+
tabulate(
|
|
137
|
+
summary_rows,
|
|
138
|
+
headers=["Action", "Count"],
|
|
139
|
+
tablefmt="github"
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
md.append("")
|
|
144
|
+
md.append(
|
|
145
|
+
f"**Total Resource Changes:** {sum(summary.values())}"
|
|
146
|
+
)
|
|
147
|
+
md.append("")
|
|
148
|
+
|
|
149
|
+
# Resource Type Summary
|
|
150
|
+
md.append("## Resource Type Summary")
|
|
151
|
+
md.append("")
|
|
152
|
+
|
|
153
|
+
type_rows = sorted(
|
|
154
|
+
type_summary.items(),
|
|
155
|
+
key=lambda x: x[1],
|
|
156
|
+
reverse=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
md.append(
|
|
160
|
+
tabulate(
|
|
161
|
+
type_rows,
|
|
162
|
+
headers=["Resource Type", "Count"],
|
|
163
|
+
tablefmt="github"
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
md.append("")
|
|
168
|
+
|
|
169
|
+
action_order = [
|
|
170
|
+
"CREATE",
|
|
171
|
+
"UPDATE",
|
|
172
|
+
"DELETE",
|
|
173
|
+
"REPLACE"
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
for action in action_order:
|
|
177
|
+
|
|
178
|
+
if action not in grouped:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
resources = sorted(
|
|
182
|
+
grouped[action],
|
|
183
|
+
key=lambda x: x["address"]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
md.append(
|
|
187
|
+
f"<details open>"
|
|
188
|
+
)
|
|
189
|
+
md.append(
|
|
190
|
+
f"<summary><strong>{action} ({len(resources)})</strong></summary>"
|
|
191
|
+
)
|
|
192
|
+
md.append("")
|
|
193
|
+
md.append("")
|
|
194
|
+
|
|
195
|
+
rows = []
|
|
196
|
+
|
|
197
|
+
for r in resources:
|
|
198
|
+
rows.append([
|
|
199
|
+
r["name"],
|
|
200
|
+
r["type"],
|
|
201
|
+
r["id"],
|
|
202
|
+
r["address"]
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
md.append(
|
|
206
|
+
tabulate(
|
|
207
|
+
rows,
|
|
208
|
+
headers=[
|
|
209
|
+
"Name",
|
|
210
|
+
"Type",
|
|
211
|
+
"Resource ID",
|
|
212
|
+
"Terraform Address"
|
|
213
|
+
],
|
|
214
|
+
tablefmt="github"
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
md.append("")
|
|
219
|
+
md.append("</details>")
|
|
220
|
+
md.append("")
|
|
221
|
+
|
|
222
|
+
return "\n".join(md)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
parser = argparse.ArgumentParser(
|
|
227
|
+
description="Generate a Markdown report from a Terraform plan."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"plan",
|
|
232
|
+
help="Terraform binary plan file (e.g. tfplan)"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
parser.add_argument(
|
|
236
|
+
"-o",
|
|
237
|
+
"--output",
|
|
238
|
+
default="README_TFPLAN.md",
|
|
239
|
+
help="Output markdown file (default: README_TFPLAN.md)"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
args = parser.parse_args()
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
plan = load_plan(args.plan)
|
|
246
|
+
|
|
247
|
+
report = build_report(plan)
|
|
248
|
+
|
|
249
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
250
|
+
f.write(report)
|
|
251
|
+
|
|
252
|
+
print(f"Report generated: {args.output}")
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print(f"Error: {e}")
|
|
256
|
+
sys.exit(1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
main()
|