Skip to content

Commit

Permalink
latest
Browse files Browse the repository at this point in the history
  • Loading branch information
morga471 committed Sep 30, 2025
1 parent 5b72d10 commit 5fdf7ae
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 855 deletions.
2 changes: 1 addition & 1 deletion local-app/python-tools/gfl-resource-actions/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ dist:

# Run in dry-run mode
run-dry-run:
$(PYTHON) -m $(PACKAGE_NAME).cli --stop --dry-run --resource-type ecr --csv-output-dir ./ --csv-export
$(PYTHON) -m $(PACKAGE_NAME).cli --stop --dry-run --resource-type ecr --csv-output-dir ./ --csv-export --csv-per-account

# Run to stop resources
run-stop:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ def parse_args():
"--csv-prefix",
help="Prefix for CSV filenames",
)
csv_group.add_argument(
"--csv-per-account",
action="store_true",
help="Create separate CSV files per account in addition to consolidated CSV",
)

return parser.parse_args()

Expand Down Expand Up @@ -189,11 +194,24 @@ def process_command(args: argparse.Namespace) -> None:

# Export to CSV
logger.info(f"Exporting resources to CSV in directory: {args.csv_output_dir}")

# Export consolidated CSV
csv_files = export_resources_to_csv(
resources=discovered_resources,
output_dir=args.csv_output_dir,
prefix=args.csv_prefix,
per_account=False, # Always create consolidated first
)

# Export per-account CSVs if requested
if args.csv_per_account:
per_account_files = export_resources_to_csv(
resources=discovered_resources,
output_dir=args.csv_output_dir,
prefix=args.csv_prefix,
per_account=True,
)
csv_files.update(per_account_files)

# Log success message with file locations
if csv_files:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def export_resources_to_csv(
resources: Dict[str, List[Dict[str, Any]]],
output_dir: Optional[str] = None,
prefix: Optional[str] = None,
per_account: bool = False,
) -> Dict[str, str]:
"""
Export discovered resources to CSV files.
Expand All @@ -187,6 +188,7 @@ def export_resources_to_csv(
resources: Dictionary of resource lists by type
output_dir: Directory to save CSV files (defaults to current directory)
prefix: Optional prefix for CSV filenames
per_account: If True, create separate CSV files per account
Returns:
Dictionary mapping resource types to their CSV file paths
Expand All @@ -197,59 +199,159 @@ def export_resources_to_csv(
# Ensure output directory exists
output_dir = ensure_directory(output_dir)

if per_account:
return _export_per_account_csv(resources, output_dir, prefix)
else:
csv_files = {}

# Process each resource type
for resource_type, resource_list in resources.items():
if not resource_list:
logger.debug(f"No {resource_type} resources to export")
continue

# Create CSV filename using utility function
filename = generate_timestamp_filename(resource_type, prefix)
filepath = os.path.join(output_dir, filename)

try:
# Extract all unique keys to use as CSV headers
all_keys = set()
for resource in resource_list:
all_keys.update(resource.keys())

# Define common fields to appear first in the CSV
common_fields = [
"id",
"name",
"accountId",
"accountName",
"region",
"status",
"type",
"tags",
]
# Sort headers with common fields first, then alphabetically
headers = [h for h in common_fields if h in all_keys]
headers.extend(sorted(k for k in all_keys if k not in common_fields))

# Initialize the CSV file with headers
initialize_csv_file(filepath, headers, overwrite=True)

# Process and write each resource
for resource in resource_list:
# Handle tags special case (convert dict to string)
if "tags" in resource and isinstance(resource["tags"], dict):
resource["tags"] = format_tags_for_csv(resource["tags"])

# Use the write_csv_row utility
write_csv_row(
filepath, {k: resource.get(k, "") for k in headers}, headers
)

logger.info(
f"Exported {len(resource_list)} {resource_type} resources to {filepath}"
)
csv_files[resource_type] = filepath

except Exception as e:
logger.error(f"Error exporting {resource_type} to CSV: {e}")

return csv_files


def _export_per_account_csv(
resources: Dict[str, List[Dict[str, Any]]],
output_dir: str,
prefix: Optional[str] = None,
) -> Dict[str, str]:
"""
Export resources to separate CSV files per account.
Returns:
Dictionary mapping account IDs to their CSV file paths
"""
csv_files = {}

# Process each resource type
# Group all resources by account
account_resources = {}
for resource_type, resource_list in resources.items():
if not resource_list:
logger.debug(f"No {resource_type} resources to export")
continue

# Create CSV filename using utility function
filename = generate_timestamp_filename(resource_type, prefix)
filepath = os.path.join(output_dir, filename)

try:
# Extract all unique keys to use as CSV headers
all_keys = set()
for resource in resource_list:
all_keys.update(resource.keys())

# Define common fields to appear first in the CSV
common_fields = [
"id",
"name",
"accountId",
"accountName",
"region",
"status",
"type",
"tags",
]
# Sort headers with common fields first, then alphabetically
headers = [h for h in common_fields if h in all_keys]
headers.extend(sorted(k for k in all_keys if k not in common_fields))

# Initialize the CSV file with headers
initialize_csv_file(filepath, headers, overwrite=True)

# Process and write each resource
for resource in resource_list:
# Handle tags special case (convert dict to string)
if "tags" in resource and isinstance(resource["tags"], dict):
resource["tags"] = format_tags_for_csv(resource["tags"])

# Use the write_csv_row utility
write_csv_row(
filepath, {k: resource.get(k, "") for k in headers}, headers
)
for resource in resource_list:
account_id = resource.get('accountId', 'unknown')
account_name = resource.get('accountName', account_id)

logger.info(
f"Exported {len(resource_list)} {resource_type} resources to {filepath}"
)
csv_files[resource_type] = filepath
if account_id not in account_resources:
account_resources[account_id] = {
'account_name': account_name,
'resources': {}
}

if resource_type not in account_resources[account_id]['resources']:
account_resources[account_id]['resources'][resource_type] = []

account_resources[account_id]['resources'][resource_type].append(resource)

# Create CSV file for each account
for account_id, account_data in account_resources.items():
account_name = account_data['account_name']

# Generate account-specific filename
safe_account_name = account_name.replace(' ', '_').replace('(', '').replace(')', '')
account_filename = generate_timestamp_filename(
f"resources_account_{account_id}_{safe_account_name}",
prefix
)
account_filepath = os.path.join(output_dir, account_filename)

# Combine all resource types for this account into one CSV
_write_account_csv(account_filepath, account_data['resources'], account_id, account_name)

except Exception as e:
logger.error(f"Error exporting {resource_type} to CSV: {e}")
csv_files[f"account_{account_id}"] = account_filepath
logger.info(f"Exported resources for account {account_name} ({account_id}) to {account_filepath}")

return csv_files


def _write_account_csv(
filepath: str,
resources_by_type: Dict[str, List[Dict[str, Any]]],
account_id: str,
account_name: str
) -> None:
"""Write all resources for an account to a single CSV file."""
all_resources = []

# Flatten all resource types into a single list
for resource_type, resource_list in resources_by_type.items():
for resource in resource_list:
# Add resource type column
resource_copy = resource.copy()
resource_copy['resource_type'] = resource_type
all_resources.append(resource_copy)

if not all_resources:
return

# Get all unique columns
all_keys = set()
for resource in all_resources:
all_keys.update(resource.keys())

# Define column order
priority_fields = [
"resource_type", "id", "name", "accountId", "accountName",
"region", "status", "type", "tags"
]
headers = [h for h in priority_fields if h in all_keys]
headers.extend(sorted(k for k in all_keys if k not in priority_fields))

# Initialize CSV with headers
initialize_csv_file(filepath, headers, overwrite=True)

# Write all resources
for resource in all_resources:
# Handle tags formatting
if "tags" in resource and isinstance(resource["tags"], dict):
resource["tags"] = format_tags_for_csv(resource["tags"])

write_csv_row(filepath, {k: resource.get(k, "") for k in headers}, headers)
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,24 @@ def ensure_directory(directory_path: Union[str, Path]) -> str:


def generate_timestamp_filename(
base_name: str, prefix: Optional[str] = None, extension: str = "csv"
base_name: str, prefix: Optional[str] = None, extension: str = "csv", account_suffix: Optional[str] = None
) -> str:
"""
Generate a filename with timestamp.
Generate a filename with timestamp and optional account suffix.
Args:
base_name: Base name for the file
prefix: Optional prefix
extension: File extension without dot
account_suffix: Optional account-specific suffix
Returns:
Timestamped filename
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
prefix_str = f"{prefix}_" if prefix else ""
return f"{prefix_str}{base_name}_{timestamp}.{extension}"
account_str = f"_{account_suffix}" if account_suffix else ""
return f"{prefix_str}{base_name}{account_str}_{timestamp}.{extension}"


def get_logs_directory(base_dir: Optional[str] = None) -> str:
Expand Down
Loading

0 comments on commit 5fdf7ae

Please sign in to comment.