Skip to content

Commit

Permalink
add support for combined tags in eks
Browse files Browse the repository at this point in the history
  • Loading branch information
morga471 committed Mar 14, 2025
1 parent a84c6fc commit 9f06bad
Show file tree
Hide file tree
Showing 4 changed files with 402 additions and 225 deletions.
91 changes: 85 additions & 6 deletions local-app/python-tools/gfl-resource-actions/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ def get_eks_clusters(credentials, region, account_id=None, account_name=None):
response = eks_client.list_clusters()
cluster_names = response.get('clusters', [])

logger.info(f"Found {len(cluster_names)} EKS clusters in region {region}")

# Get detailed info for each cluster
for cluster_name in cluster_names:
try:
Expand All @@ -271,19 +273,66 @@ def get_eks_clusters(credentials, region, account_id=None, account_name=None):
# Get tags
tags = cluster_info.get('tags', {})

# Look for both individual scaling tags and combined format
min_nodes = tags.get('eks-min-nodes', '')
max_nodes = tags.get('eks-max-nodes', '')
desired_nodes = tags.get('eks-desired-nodes', '')

# Parse the combined cluster:size tag if it exists
if 'cluster:size' in tags:
size_tag = tags['cluster:size']
logger.debug(f"Found cluster:size tag: {size_tag} for cluster {cluster_name}")
try:
# Parse min:X-max:Y-desired:Z format
if 'min:' in size_tag and 'max:' in size_tag and 'desired:' in size_tag:
# Extract values using regex or string parsing
min_match = size_tag.split('min:')[1].split('-')[0] if 'min:' in size_tag else None
max_match = size_tag.split('max:')[1].split('-')[0] if 'max:' in size_tag else None
desired_match = size_tag.split('desired:')[1].split('-')[0] if 'desired:' in size_tag else None

min_nodes = min_match if min_match and not min_nodes else min_nodes
max_nodes = max_match if max_match and not max_nodes else max_nodes
desired_nodes = desired_match if desired_match and not desired_nodes else desired_nodes

logger.debug(f"Parsed cluster:size: min={min_nodes}, max={max_nodes}, desired={desired_nodes}")
except Exception as parse_error:
logger.warning(f"Error parsing cluster:size tag for cluster {cluster_name}: {parse_error}")

# Check for backup tags that store original values
original_min = tags.get('eks-original-min-nodes', '')
original_max = tags.get('eks-original-max-nodes', '')
original_desired = tags.get('eks-original-desired-nodes', '')

# Check for schedule tag
schedule = None
for tag_key, tag_value in tags.items():
if 'schedule' in tag_key.lower():
schedule = tag_value
break

cluster_data = {
"name": cluster_name,
"id": cluster_name, # Using name as ID for consistency
"status": cluster_info.get('status', 'UNKNOWN'),
"region": region,
"endpoint": cluster_info.get('endpoint', ''),
"version": cluster_info.get('version', ''),
"min_nodes": min_nodes,
"max_nodes": max_nodes,
"desired_nodes": desired_nodes,
"original_min": original_min,
"original_max": original_max,
"original_desired": original_desired,
"schedule": schedule,
"tags": tags
}

clusters.append(cluster_data)

# Log the discovery
# Log the discovery with scaling info and schedule
scale_details = f"Min: {min_nodes or 'N/A'}, Max: {max_nodes or 'N/A'}, Desired: {desired_nodes or 'N/A'}"
schedule_info = f", Schedule: {schedule}" if schedule else ""

if account_id:
log_action_to_csv(
account_id=account_id,
Expand All @@ -294,7 +343,8 @@ def get_eks_clusters(credentials, region, account_id=None, account_name=None):
action="discover",
region=region,
status="success",
details=f"Status: {cluster_data['status']}, Version: {cluster_data['version']}"
details=f"Status: {cluster_data['status']}, Version: {cluster_data['version']}, Scaling: {scale_details}{schedule_info}",
existing_schedule=schedule
)

except Exception as e:
Expand All @@ -312,19 +362,37 @@ def get_eks_clusters(credentials, region, account_id=None, account_name=None):
# Get tags
tags = cluster_info.get('tags', {})

# Extract scaling parameters from tags if they exist
min_nodes = tags.get('eks-min-nodes', '')
max_nodes = tags.get('eks-max-nodes', '')
desired_nodes = tags.get('eks-desired-nodes', '')

# Check for backup tags that store original values
original_min = tags.get('eks-original-min-nodes', '')
original_max = tags.get('eks-original-max-nodes', '')
original_desired = tags.get('eks-original-desired-nodes', '')

cluster_data = {
"name": cluster_name,
"id": cluster_name, # Using name as ID for consistency
"status": cluster_info.get('status', 'UNKNOWN'),
"region": region,
"endpoint": cluster_info.get('endpoint', ''),
"version": cluster_info.get('version', ''),
"min_nodes": min_nodes,
"max_nodes": max_nodes,
"desired_nodes": desired_nodes,
"original_min": original_min,
"original_max": original_max,
"original_desired": original_desired,
"tags": tags
}

clusters.append(cluster_data)

# Log the discovery
# Log the discovery with scaling info
scale_details = f"Min: {min_nodes or 'N/A'}, Max: {max_nodes or 'N/A'}, Desired: {desired_nodes or 'N/A'}"

if account_id:
log_action_to_csv(
account_id=account_id,
Expand All @@ -335,13 +403,24 @@ def get_eks_clusters(credentials, region, account_id=None, account_name=None):
action="discover",
region=region,
status="success",
details=f"Status: {cluster_data['status']}, Version: {cluster_data['version']}"
details=f"Status: {cluster_data['status']}, Version: {cluster_data['version']}, Scaling: {scale_details}"
)

except Exception as e:
logger.warning(f"Error getting details for EKS cluster {cluster_name} in region {region}: {e}")

logger.info(f"Found {len(clusters)} EKS clusters in region {region}")
# Log the summary
scaling_stats = {}
for cluster in clusters:
has_scaling = any([cluster.get('min_nodes'), cluster.get('max_nodes'), cluster.get('desired_nodes')])
scaling_stats["with_scaling_tags"] = scaling_stats.get("with_scaling_tags", 0) + (1 if has_scaling else 0)
scaling_stats["without_scaling_tags"] = scaling_stats.get("without_scaling_tags", 0) + (0 if has_scaling else 1)

if scaling_stats:
logger.info(f"EKS clusters in {region}: {len(clusters)} total, "
f"{scaling_stats.get('with_scaling_tags', 0)} with scaling tags, "
f"{scaling_stats.get('without_scaling_tags', 0)} without scaling tags")

return clusters

except Exception as e:
Expand Down Expand Up @@ -370,7 +449,7 @@ def get_emr_clusters(credentials, region, account_id=None, account_name=None):
emr_client = create_boto3_client('emr', credentials, region)
clusters = []

# List active clusters (RUNNING, WAITING, STARTING, BOOTSTRAPPING, TERMINATING)
# List active clusters (RUNNING, WAITING, STARTING, BOOTSTRAPPING, TERMINATING, TERMINATED_WITH_ERRORS)
response = emr_client.list_clusters(
ClusterStates=['RUNNING', 'WAITING', 'STARTING', 'BOOTSTRAPPING', 'TERMINATING', 'TERMINATED_WITH_ERRORS']
)
Expand Down
65 changes: 31 additions & 34 deletions local-app/python-tools/gfl-resource-actions/logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,43 +69,40 @@ def initialize_csv_log(csv_file=None):

return csv_path

def log_action_to_csv(account_id, account_name, resource_type, resource_id,
resource_name, action, region, status, details='', csv_file=None):
"""
Log an action to the CSV file.
def log_action_to_csv(account_id, account_name, resource_type, resource_id, resource_name, action, region, status, details="", dry_run=False, existing_schedule=None):
"""Log resource action to CSV file for tracking."""
timestamp = datetime.datetime.now().isoformat()

Args:
account_id (str): AWS account ID
account_name (str): AWS account name
resource_type (str): Type of resource (e.g., 'ec2', 'rds')
resource_id (str): Resource identifier
resource_name (str): Resource name
action (str): Action performed (e.g., 'discover', 'stop', 'start')
region (str): AWS region
status (str): Status of the action (e.g., 'success', 'failed')
details (str, optional): Additional details
csv_file (str, optional): CSV file name (default: resource_actions.csv)
"""
if csv_file is None:
csv_file = ACTION_CSV_FILE
# Create directory if it doesn't exist
os.makedirs(os.path.dirname(CSV_LOG_DIR), exist_ok=True)

csv_path = initialize_csv_log(csv_file)
timestamp = datetime.datetime.now().isoformat()
# Check if file exists to determine if we need to write headers
file_exists = os.path.isfile(CSV_LOG_DIR)

with open(csv_path, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerow([
timestamp,
account_id,
account_name,
resource_type,
resource_id,
resource_name,
action,
region,
status,
details
])
with open(CSV_LOG_DIR, 'a', newline='') as csvfile:
fieldnames = ['timestamp', 'account_id', 'account_name', 'resource_type', 'resource_id',
'resource_name', 'action', 'region', 'status', 'details', 'dry_run', 'schedule']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

# Write header if file is new
if not file_exists:
writer.writeheader()

# Write the log entry
writer.writerow({
'timestamp': timestamp,
'account_id': account_id,
'account_name': account_name,
'resource_type': resource_type,
'resource_id': resource_id,
'resource_name': resource_name,
'action': action,
'region': region,
'status': status,
'details': details,
'dry_run': 'Yes' if dry_run else 'No',
'schedule': existing_schedule or ''
})

# Create a logger instance for direct import
logger = setup_logging()
25 changes: 15 additions & 10 deletions local-app/python-tools/gfl-resource-actions/managers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,21 @@ def get_timestamp(self):
"""Get ISO 8601 timestamp."""
return datetime.datetime.now().isoformat()

def log_action(self, resource_id, region, action, resource_name=None, dry_run=False):
"""Log resource action."""
action_prefix = "[DRY RUN] " if dry_run else ""
resource_info = f"name={resource_name}, " if resource_name else ""
timestamp = self.get_timestamp()

logger.info(f"{action_prefix}ACTION: resource={resource_id}, {resource_info}region={region}, "
f"account_id={self.account_id}, action={action}, timestamp={timestamp}")

return timestamp
def log_action(self, resource_id, region, action, details="", dry_run=False, existing_schedule=None):
"""Log a resource action to CSV for audit trail."""
log_action_to_csv(
account_id=self.account_id,
account_name=self.account_name,
resource_type=self.resource_type,
resource_id=resource_id,
resource_name=resource_id, # Using ID as name for simplicity
action=action,
region=region,
status="simulated" if dry_run else "success",
details=details,
dry_run=dry_run,
existing_schedule=existing_schedule
)

def should_exclude(self, resource):
"""Check if resource should be excluded based on tags."""
Expand Down
Loading

0 comments on commit 9f06bad

Please sign in to comment.