Skip to content

Commit 7df7577

Browse files
committed
refactor
1 parent 00d3927 commit 7df7577

File tree

2 files changed

+74
-65
lines changed

2 files changed

+74
-65
lines changed

src/tfblocks/main.py

Lines changed: 73 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ def file_exists(file_path: str) -> bool:
2121
"""Check if a file exists."""
2222
return os.path.exists(file_path)
2323

24+
2425
def extract_resource_addresses_from_content(content: str) -> List[str]:
2526
"""Extract resource and module addresses from Terraform content."""
2627
addresses = []
27-
28+
2829
# Match resource blocks
2930
resource_pattern = r'resource\s+"([^"]+)"\s+"([^"]+)"'
3031
resources = re.finditer(resource_pattern, content)
@@ -36,35 +37,39 @@ def extract_resource_addresses_from_content(content: str) -> List[str]:
3637
modules = re.finditer(module_pattern, content)
3738
for match in modules:
3839
addresses.append(f"module.{match.group(1)}")
39-
40+
4041
return addresses
4142

43+
4244
def extract_resource_addresses_from_file(file_path: str) -> List[str]:
4345
"""Extract resource and module addresses from a Terraform file."""
4446
addresses = []
45-
47+
4648
# First check if file exists
4749
if not file_exists(file_path):
4850
print(f"Error: File {file_path} does not exist", file=sys.stderr)
4951
sys.exit(1)
50-
52+
5153
try:
5254
with open(file_path, "r") as f:
5355
content = f.read()
54-
56+
5557
addresses = extract_resource_addresses_from_content(content)
5658

5759
except Exception as e:
5860
print(f"Warning: Could not process file {file_path}: {str(e)}", file=sys.stderr)
5961

6062
if not addresses:
61-
print(f"Warning: No resources or modules found in file {file_path}", file=sys.stderr)
62-
63+
print(
64+
f"Warning: No resources or modules found in file {file_path}",
65+
file=sys.stderr,
66+
)
67+
6368
return addresses
6469

6570

6671
def is_resource_match(
67-
resource_addr: str, filter_addrs: List[str], file_addrs: List[str]
72+
resource_addr: str, filter_addrs: List[str] = [], file_addrs: List[str] = []
6873
) -> bool:
6974
"""Check if resource address matches the filter conditions.
7075
@@ -106,25 +111,26 @@ def matches_address_list(addr: str, addr_list: List[str]) -> bool:
106111
if base_addr_match:
107112
# Get the base address and any remaining part after the index
108113
base_addr = base_addr_match.group(1)
109-
remaining = base_addr_match.group(3)
110114

111115
# Check if the base address matches the filter
112116
if base_addr == filter_addr:
113117
return True
114118

115119
# Handle nested modules with indices
116-
if filter_addr.startswith("module.") and base_addr.startswith(f"{filter_addr}."):
120+
if filter_addr.startswith("module.") and base_addr.startswith(
121+
f"{filter_addr}."
122+
):
117123
return True
118124

119125
# Standard module prefix match (for non-indexed modules)
120126
if filter_addr.startswith("module.") and addr.startswith(f"{filter_addr}."):
121127
return True
122-
128+
123129
# For file addresses, we need to check if the resource type and name match
124130
# even if the full address is different due to modules
125131
resource_type, resource_name = extract_resource_type_and_name(addr)
126132
filter_type, filter_name = extract_resource_type_and_name(filter_addr)
127-
133+
128134
if resource_type and resource_name and filter_type and filter_name:
129135
if resource_type == filter_type and resource_name == filter_name:
130136
return True
@@ -152,23 +158,25 @@ def matches_address_list(addr: str, addr_list: List[str]) -> bool:
152158
def filter_resources(
153159
state: Dict[str, Any], addresses: List[str] = [], files: List[str] = []
154160
) -> List[Dict[str, Any]]:
155-
"""Extract matching AWS managed resources from state."""
161+
"""Extract matching AWS resources from Terraform state."""
156162
# Extract addresses from files if provided
157163
file_addresses = []
158164
if files:
159165
for file_path in files:
160166
extracted = extract_resource_addresses_from_file(file_path)
161167
file_addresses.extend(extracted)
162-
168+
163169
if not file_addresses:
164-
print("Warning: No resources or modules found in any of the specified files.", file=sys.stderr)
170+
print(
171+
"Warning: No resources or modules found in any of the specified files.",
172+
file=sys.stderr,
173+
)
165174
print("No resources will be included in the output.", file=sys.stderr)
166175
return []
167176

168177
resources = []
169178
modules_to_process = [state["values"]["root_module"]]
170179

171-
# Iterative approach instead of recursion
172180
while modules_to_process:
173181
module = modules_to_process.pop()
174182

@@ -185,15 +193,18 @@ def filter_resources(
185193
resources.append(resource)
186194

187195
if not resources and (addresses or files):
188-
print("Warning: No resources found matching the specified filters.", file=sys.stderr)
196+
print(
197+
"Warning: No resources found matching the specified filters.",
198+
file=sys.stderr,
199+
)
189200

190201
return resources
191202

192203

193204
def generate_import_block(
194205
resource: Dict[str, Any], schema_classes: Dict[str, type]
195206
) -> str:
196-
"""Generate import block for a resource."""
207+
"""Generate Terraform import block for a resource."""
197208
matching_class = schema_classes.get(resource["type"])
198209
documentation = f"https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/{resource['type'].replace('aws_', '')}#import"
199210
import_id = f'"" # TODO: {documentation}'
@@ -213,7 +224,7 @@ def generate_import_block(
213224

214225

215226
def generate_removed_block(resource_addr: str, destroy: bool = False) -> str:
216-
"""Generate removed block for a resource or module."""
227+
"""Generate Terraform removed block for a resource or module."""
217228
destroy_line = "\n destroy = true" if destroy else "\n destroy = false"
218229

219230
# Strip instance keys from the resource address for the 'from' attribute
@@ -227,23 +238,52 @@ def generate_removed_block(resource_addr: str, destroy: bool = False) -> str:
227238
}}"""
228239

229240

241+
def generate_blocks_for_command(
242+
resources: List[Dict[str, Any]], command: str, destroy: bool = False
243+
) -> List[str]:
244+
"""Generate Terraform code blocks based on command."""
245+
blocks = []
246+
247+
if command == "remove":
248+
# For removed blocks, deduplicate based on the base address
249+
base_addresses = set()
250+
251+
for resource in resources:
252+
# Strip instance keys to get the base address
253+
base_addr = re.sub(
254+
r'(\[\d+\]|\[".+?"\]|\[\'.+?\'\])', "", resource["address"]
255+
)
256+
257+
# Only add a removed block if we haven't seen this base address before
258+
if base_addr not in base_addresses:
259+
base_addresses.add(base_addr)
260+
blocks.append(generate_removed_block(resource["address"], destroy))
261+
elif command == "import":
262+
# For import blocks, we need the full resource data
263+
schema_classes = get_aws_resource_import_id_generators()
264+
blocks = [generate_import_block(r, schema_classes) for r in resources]
265+
else:
266+
raise ValueError(f"Invalid command '{command}'")
267+
return blocks
268+
269+
230270
def parse_args() -> argparse.Namespace:
231271
"""Parse command line arguments."""
232272
parser = argparse.ArgumentParser(
233273
description="Terraform blocks utility for generating and managing Terraform blocks",
234-
epilog="Example usage: terraform show -json | tfblocks import [resource_addresses]"
274+
epilog="Example usage: terraform show -json | tfblocks import [resource_addresses]",
235275
)
236276

237-
# Global options
238277
parser.add_argument(
239278
"--no-color", action="store_true", help="Disable colored output"
240279
)
241280

242-
# Create subcommands
243-
subparsers = parser.add_subparsers(dest="command", help="Command to execute", required=True)
281+
subparsers = parser.add_subparsers(
282+
dest="command", help="Command to execute", required=True
283+
)
244284

245285
# Common filter arguments function
246-
def add_filter_args(cmd_parser):
286+
def add_filter_args(cmd_parser: argparse.ArgumentParser):
247287
cmd_parser.add_argument(
248288
"addresses",
249289
nargs="*",
@@ -253,11 +293,9 @@ def add_filter_args(cmd_parser):
253293
"--files", "-f", nargs="+", help="Optional Terraform files to filter by"
254294
)
255295

256-
# Import command
257296
import_parser = subparsers.add_parser("import", help="Generate import blocks")
258297
add_filter_args(import_parser)
259298

260-
# Remove command
261299
remove_parser = subparsers.add_parser("remove", help="Generate removed blocks")
262300
add_filter_args(remove_parser)
263301
remove_parser.add_argument(
@@ -266,73 +304,44 @@ def add_filter_args(cmd_parser):
266304
help="Set destroy = true in removed blocks (default is false)",
267305
)
268306

269-
# List command
270-
list_parser = subparsers.add_parser("list", help="List resource addresses")
307+
list_parser = subparsers.add_parser(
308+
"list", help="List addresses delimited by newlines"
309+
)
271310
add_filter_args(list_parser)
272311

273-
# No default command - require explicit command
274312
args = parser.parse_args()
275313
return args
276314

277315

278-
def generate_blocks_for_command(
279-
resources: List[Dict[str, Any]], command: str, destroy: bool = False
280-
) -> List[str]:
281-
"""Generate appropriate blocks based on command."""
282-
blocks = []
283-
284-
if command == "remove":
285-
# For removed blocks, deduplicate based on the base address
286-
base_addresses = set()
287-
288-
for resource in resources:
289-
# Strip instance keys to get the base address
290-
base_addr = re.sub(
291-
r'(\[\d+\]|\[".+?"\]|\[\'.+?\'\])', "", resource["address"]
292-
)
293-
294-
# Only add a removed block if we haven't seen this base address before
295-
if base_addr not in base_addresses:
296-
base_addresses.add(base_addr)
297-
blocks.append(generate_removed_block(resource["address"], destroy))
298-
else: # import command
299-
# For import blocks, we need the full resource data
300-
schema_classes = get_aws_resource_import_id_generators()
301-
blocks = [generate_import_block(r, schema_classes) for r in resources]
302-
303-
return blocks
304-
305-
306316
def main():
307317
args = parse_args()
308-
309318
state = json.load(sys.stdin)
310-
311319
if state.get("format_version") != "1.0":
312320
print(
313321
"Error: Unsupported state file format version. Expected version '1.0'.",
314322
file=sys.stderr,
315323
)
316324
sys.exit(1)
317-
318325
resources = filter_resources(state, args.addresses, args.files)
319326

320327
if args.command == "list":
321-
# Just output the addresses
322328
for resource in resources:
323329
print(resource["address"])
324330
return
325331

326-
# Generate blocks based on command
327332
blocks = generate_blocks_for_command(
328333
resources, args.command, getattr(args, "destroy", False)
329334
)
330335

331-
# Output results
332336
if args.no_color:
333337
print("\n\n".join(blocks))
334338
else:
335-
print("\033[92m" + "\n\n".join(blocks) + "\033[0m")
339+
if args.command == "import":
340+
print("\033[92m" + "\n\n".join(blocks) + "\033[0m")
341+
elif args.command == "remove":
342+
print("\033[91m" + "\n\n".join(blocks) + "\033[0m")
343+
else:
344+
print("\033[92m" + "\n\n".join(blocks) + "\033[0m")
336345

337346

338347
if __name__ == "__main__":

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)