Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 63 additions & 51 deletions integration/setup/companion-stack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ Resources:
Resource:
- !Sub "arn:${AWS::Partition}:s3:::*sam-integ-stack-*"
- !Sub "arn:${AWS::Partition}:s3:::*sam-integ-stack-*/*"
- Effect: Allow
Action:
- apigateway:DELETE
Comment thread
vicheey marked this conversation as resolved.
Resource: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}::/restapis/*"
- Effect: Allow
Action:
- logs:DeleteLogGroup
Expand All @@ -324,8 +328,7 @@ Resources:
import boto3, time
from datetime import datetime, timezone, timedelta

STACK_PATTERN = 'sam-integ-stack-'
IAM_PATTERN = 'sam-integ-'
TEST_PATTERN = 'sam-integ-'
ELIGIBLE_STATUSES = [
'CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'ROLLBACK_FAILED',
'REVIEW_IN_PROGRESS', 'DELETE_FAILED', 'UPDATE_FAILED',
Expand All @@ -335,106 +338,115 @@ Resources:
def _has_time(ctx):
return ctx.get_remaining_time_in_millis() > 30000

def _is_test(name, strict=False):
pattern = STACK_PATTERN if strict else IAM_PATTERN
return pattern in name and 'companion' not in name
def _is_test(name):
return TEST_PATTERN in name and 'companion' not in name

def handler(event, ctx):
cfn = boto3.client('cloudformation')
iam = boto3.client('iam')
logs = boto3.client('logs')
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)

apig = boto3.client('apigateway')
cutoff = datetime.now(timezone.utc) - timedelta(hours=6)
_sweep_apis(apig, cutoff, ctx)
_sweep_stacks(cfn, iam, cutoff, ctx)
_sweep_log_groups(logs, cutoff, ctx)
_sweep_logs(logs, cutoff, ctx)

def _sweep_apis(apig, cutoff, ctx):
try:
for page in apig.get_paginator('get_rest_apis').paginate():
for api in page['items']:
if ctx.get_remaining_time_in_millis() < 420000: return
if not _is_test(api.get('name', '')): continue
c = api.get('createdDate')
if c and c.replace(tzinfo=timezone.utc) >= cutoff: continue
try:
print(f"Deleting API: {api['name']}")
apig.delete_rest_api(restApiId=api['id'])
except Exception: pass
time.sleep(35)
except Exception as e: print(f"api err: {e}")

def _sweep_stacks(cfn, iam, cutoff, ctx):
deleted = []
for page in cfn.get_paginator('list_stacks').paginate(StackStatusFilter=ELIGIBLE_STATUSES):
for stack in page['StackSummaries']:
for s in page['StackSummaries']:
if not _has_time(ctx):
print(f"Attempt to delete ({len(deleted)}) stacks: {deleted}")
return
name = stack['StackName']
if not _is_test(name, strict=True):
name = s['StackName']
if not _is_test(name):
continue
if stack['CreationTime'].replace(tzinfo=timezone.utc) >= cutoff:
if s['CreationTime'].replace(tzinfo=timezone.utc) >= cutoff:
continue
if stack['StackStatus'] == 'DELETE_FAILED':
Comment thread
vicheey marked this conversation as resolved.
if s['StackStatus'] == 'DELETE_FAILED':
_fix_and_retry(cfn, iam, name)
try:
print(f"Deleting: {name}")
cfn.delete_stack(StackName=name)
deleted.append(name)
time.sleep(1)
except Exception as e:
print(f"delete_stack {name}: {e}")
print(f"Attempt to delete ({len(deleted)}) stacks: {deleted}")
except Exception: pass

def _fix_and_retry(cfn, iam, stack_name):
def _fix_and_retry(cfn, iam, name):
try:
Comment thread
vicheey marked this conversation as resolved.
for event in cfn.describe_stack_events(StackName=stack_name)['StackEvents']:
if event.get('ResourceStatus') != 'DELETE_FAILED':
for ev in cfn.describe_stack_events(StackName=name)['StackEvents']:
if ev.get('ResourceStatus') != 'DELETE_FAILED':
continue
resource_type = event.get('ResourceType', '')
resource_id = event.get('PhysicalResourceId', '')
if not resource_id or not _is_test(resource_id):
rt = ev.get('ResourceType', '')
rid = ev.get('PhysicalResourceId', '')
if not rid or not _is_test(rid):
continue
if resource_type == 'AWS::IAM::Role':
_force_delete_role(iam, resource_id)
elif resource_type == 'AWS::IAM::Policy':
_force_delete_policy(iam, resource_id)
elif resource_type == 'AWS::S3::Bucket':
if rt == 'AWS::IAM::Role':
_force_delete_role(iam, rid)
elif rt == 'AWS::IAM::Policy':
_force_delete_policy(iam, rid)
elif rt == 'AWS::S3::Bucket':
try:
bucket = boto3.resource('s3').Bucket(resource_id)
bucket.object_versions.delete()
bucket.objects.delete()
b = boto3.resource('s3').Bucket(rid)
b.object_versions.delete()
b.objects.delete()
except Exception: pass
except Exception as e:
print(f"fix_and_retry {stack_name}: {e}")
print(f"fix {name}: {e}")
Comment thread
vicheey marked this conversation as resolved.

def _force_delete_role(iam, role_name):
def _force_delete_role(iam, role):
try:
for p in iam.list_role_policies(RoleName=role_name)['PolicyNames']:
iam.delete_role_policy(RoleName=role_name, PolicyName=p)
for p in iam.list_attached_role_policies(RoleName=role_name)['AttachedPolicies']:
iam.detach_role_policy(RoleName=role_name, PolicyArn=p['PolicyArn'])
iam.delete_role(RoleName=role_name)
for p in iam.list_role_policies(RoleName=role)['PolicyNames']:
iam.delete_role_policy(RoleName=role, PolicyName=p)
for p in iam.list_attached_role_policies(RoleName=role)['AttachedPolicies']:
iam.detach_role_policy(RoleName=role, PolicyArn=p['PolicyArn'])
iam.delete_role(RoleName=role)
except Exception: pass

def _force_delete_policy(iam, arn):
try:
for page in iam.get_paginator('list_entities_for_policy').paginate(PolicyArn=arn, EntityFilter='Role'):
for r in page['PolicyRoles']:
for pg in iam.get_paginator('list_entities_for_policy').paginate(PolicyArn=arn, EntityFilter='Role'):
for r in pg['PolicyRoles']:
iam.detach_role_policy(RoleName=r['RoleName'], PolicyArn=arn)
for v in iam.list_policy_versions(PolicyArn=arn)['Versions']:
if not v['IsDefaultVersion']:
iam.delete_policy_version(PolicyArn=arn, VersionId=v['VersionId'])
iam.delete_policy(PolicyArn=arn)
except Exception: pass

def _sweep_log_groups(logs, cutoff, ctx):
def _sweep_logs(logs, cutoff, ctx):
cutoff_ms = int(cutoff.timestamp() * 1000)
deleted = 0
for page in logs.get_paginator('describe_log_groups').paginate():
for log_group in page['logGroups']:
for lg in page['logGroups']:
if not _has_time(ctx):
return
name = log_group['logGroupName']
if STACK_PATTERN not in name:
name = lg['logGroupName']
if not _is_test(name):
continue
if log_group.get('creationTime', 0) >= cutoff_ms:
if lg.get('creationTime', 0) >= cutoff_ms:
continue
try:
print(f"Deleting: {name}")
logs.delete_log_group(logGroupName=name)
deleted += 1
time.sleep(1)
except Exception: pass
print(f"Log groups: {deleted} deleted")
Comment thread
vicheey marked this conversation as resolved.

TestStackSweeperSchedule:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: rate(6 hours)
ScheduleExpression: rate(30 minutes)
Comment thread
vicheey marked this conversation as resolved.
State: ENABLED
Targets:
- Arn: !GetAtt TestStackSweeperFunction.Arn
Expand Down