Files
tracer-cloud--opensre/tests/shared/infrastructure_sdk/cleanup.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

275 lines
9.4 KiB
Python

"""Tag-based resource cleanup."""
import logging
import time
from typing import Any
from botocore.exceptions import ClientError
from tests.shared.infrastructure_sdk.deployer import get_boto3_client
logger = logging.getLogger(__name__)
# Resource types and their deletion order (dependencies matter)
RESOURCE_DELETION_ORDER = [
"elasticloadbalancing:loadbalancer",
"elasticloadbalancing:targetgroup",
"ecs:service",
"ecs:task",
"ecs:cluster",
"apigateway:restapis",
"lambda:function",
"iam:role",
"s3:bucket",
"logs:log-group",
"ecr:repository",
"ec2:security-group",
]
def find_resources_by_stack(stack_name: str, region: str = "us-east-1") -> dict[str, list[str]]:
"""Find all resources tagged with stack name using Resource Groups Tagging API.
Args:
stack_name: The stack name to search for.
region: AWS region.
Returns:
Dictionary mapping resource type to list of resource ARNs.
"""
client = get_boto3_client("resourcegroupstaggingapi", region)
resources: dict[str, list[str]] = {}
paginator = client.get_paginator("get_resources")
for page in paginator.paginate(
TagFilters=[
{"Key": "tracer:stack", "Values": [stack_name]},
{"Key": "tracer:managed", "Values": ["sdk"]},
]
):
for resource in page.get("ResourceTagMappingList", []):
arn = resource["ResourceARN"]
# Extract resource type from ARN (e.g., arn:aws:s3:::bucket -> s3:bucket)
resource_type = _get_resource_type_from_arn(arn)
if resource_type not in resources:
resources[resource_type] = []
resources[resource_type].append(arn)
return resources
def _get_resource_type_from_arn(arn: str) -> str:
"""Extract resource type from ARN."""
# ARN format: arn:aws:service:region:account:resource-type/resource-id
parts = arn.split(":")
if len(parts) < 6:
return "unknown"
service = parts[2]
resource_part = parts[5]
# Handle different ARN formats
if "/" in resource_part:
resource_type = resource_part.split("/")[0]
else:
resource_type = resource_part
return f"{service}:{resource_type}"
def destroy_stack(stack_name: str, region: str = "us-east-1") -> dict[str, Any]:
"""Delete all resources for a stack in correct dependency order.
Order: Services -> Tasks -> Clusters -> APIs -> Lambdas -> Roles -> Buckets -> Log Groups
Args:
stack_name: Name of the stack to destroy.
region: AWS region.
Returns:
Dictionary with deletion results.
"""
from tests.shared.infrastructure_sdk.config import delete_outputs
resources = find_resources_by_stack(stack_name, region)
results: dict[str, Any] = {"deleted": [], "failed": [], "not_found": []}
# Delete in dependency order
for resource_type in RESOURCE_DELETION_ORDER:
if resource_type not in resources:
continue
for arn in resources[resource_type]:
try:
_delete_resource(arn, resource_type, region)
results["deleted"].append(arn)
except ClientError as e:
if e.response["Error"]["Code"] in [
"NoSuchEntity",
"ResourceNotFoundException",
"404",
]:
results["not_found"].append(arn)
else:
results["failed"].append({"arn": arn, "error": str(e)})
except Exception as e:
results["failed"].append({"arn": arn, "error": str(e)})
# Delete any remaining resources not in our ordered list
for resource_type, arns in resources.items():
if resource_type in RESOURCE_DELETION_ORDER:
continue
for arn in arns:
if arn not in results["deleted"] and arn not in [
r["arn"] for r in results["failed"] if isinstance(r, dict)
]:
try:
_delete_resource(arn, resource_type, region)
results["deleted"].append(arn)
except Exception as e:
results["failed"].append({"arn": arn, "error": str(e)})
# Delete outputs file
delete_outputs(stack_name)
return results
def _delete_resource(arn: str, resource_type: str, region: str) -> None:
"""Delete a single resource by ARN."""
if resource_type.startswith("ecs:service"):
_delete_ecs_service(arn, region)
elif resource_type.startswith("ecs:cluster"):
_delete_ecs_cluster(arn, region)
elif resource_type.startswith("apigateway"):
_delete_api_gateway(arn, region)
elif resource_type.startswith("lambda"):
_delete_lambda(arn, region)
elif resource_type.startswith("iam:role"):
_delete_iam_role(arn, region)
elif resource_type.startswith("s3"):
_delete_s3_bucket(arn, region)
elif resource_type.startswith("logs"):
_delete_log_group(arn, region)
elif resource_type.startswith("ecr"):
_delete_ecr_repository(arn, region)
elif resource_type.startswith("ec2:security-group"):
_delete_security_group(arn, region)
def _delete_ecs_service(arn: str, region: str) -> None:
"""Delete ECS service."""
client = get_boto3_client("ecs", region)
# Extract cluster and service name from ARN
# Format: arn:aws:ecs:region:account:service/cluster-name/service-name
parts = arn.split("/")
if len(parts) >= 3:
cluster = parts[-2]
service = parts[-1]
# Scale to 0 first
try:
client.update_service(cluster=cluster, service=service, desiredCount=0)
time.sleep(5)
except ClientError:
# Service may be draining or already at 0; proceed with deletion
logger.debug("Could not scale ECS service before deletion", exc_info=True)
client.delete_service(cluster=cluster, service=service, force=True)
def _delete_ecs_cluster(arn: str, region: str) -> None:
"""Delete ECS cluster."""
client = get_boto3_client("ecs", region)
# Extract cluster name from ARN
cluster_name = arn.split("/")[-1]
client.delete_cluster(cluster=cluster_name)
def _delete_api_gateway(arn: str, region: str) -> None:
"""Delete API Gateway."""
client = get_boto3_client("apigateway", region)
# Extract API ID from ARN
api_id = arn.split("/")[-1]
client.delete_rest_api(restApiId=api_id)
def _delete_lambda(arn: str, region: str) -> None:
"""Delete Lambda function."""
client = get_boto3_client("lambda", region)
# Extract function name from ARN
function_name = arn.split(":")[-1]
client.delete_function(FunctionName=function_name)
def _delete_iam_role(arn: str, region: str) -> None:
"""Delete IAM role and all attached policies."""
client = get_boto3_client("iam", region)
role_name = arn.split("/")[-1]
# Detach managed policies
try:
attached = client.list_attached_role_policies(RoleName=role_name)
for policy in attached.get("AttachedPolicies", []):
client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
except ClientError:
# Role may not exist or policies already detached
logger.debug("Could not detach managed role policies", exc_info=True)
# Delete inline policies
try:
inline = client.list_role_policies(RoleName=role_name)
for policy_name in inline.get("PolicyNames", []):
client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
except ClientError:
# Role may not exist or inline policies already deleted
logger.debug("Could not delete inline role policies", exc_info=True)
# Delete instance profiles
try:
profiles = client.list_instance_profiles_for_role(RoleName=role_name)
for profile in profiles.get("InstanceProfiles", []):
client.remove_role_from_instance_profile(
InstanceProfileName=profile["InstanceProfileName"],
RoleName=role_name,
)
except ClientError:
# Role may not exist or instance profiles already removed
logger.debug("Could not remove role from instance profiles", exc_info=True)
client.delete_role(RoleName=role_name)
def _delete_s3_bucket(arn: str, region: str) -> None:
"""Delete S3 bucket and all objects."""
from tests.shared.infrastructure_sdk.resources.s3 import delete_bucket
# Extract bucket name from ARN (arn:aws:s3:::bucket-name)
bucket_name = arn.split(":")[-1]
delete_bucket(bucket_name, region)
def _delete_log_group(arn: str, region: str) -> None:
"""Delete CloudWatch log group."""
client = get_boto3_client("logs", region)
# Extract log group name from ARN
# Format: arn:aws:logs:region:account:log-group:name:*
parts = arn.split(":")
if len(parts) >= 7:
log_group_name = parts[6]
client.delete_log_group(logGroupName=log_group_name)
def _delete_ecr_repository(arn: str, region: str) -> None:
"""Delete ECR repository and all images."""
client = get_boto3_client("ecr", region)
# Extract repository name from ARN
repo_name = arn.split("/")[-1]
client.delete_repository(repositoryName=repo_name, force=True)
def _delete_security_group(arn: str, region: str) -> None:
"""Delete security group."""
client = get_boto3_client("ec2", region)
# Extract security group ID from ARN
sg_id = arn.split("/")[-1]
client.delete_security_group(GroupId=sg_id)