Skip to content

Commit bbc569c

Browse files
authored
Feat/update ec2 add s3 (#18)
* refactor(ec2): move account ID retrieval to common module * feat(s3): implement S3 bucket cleanup functionality * feat(s3): enhance bucket cataloging by region filtering * feat(s3): implement object cataloging and cleanup functions * refactor(tests): rename test functions for consistency * feat(orchestrator): enhance service orchestration with task tracking * test(orchestrator): enhance service orchestration tests * feat(tests): add comprehensive tests for costcutter services
1 parent fa82ada commit bbc569c

21 files changed

+1052
-77
lines changed

src/costcutter/conf/config.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
dry_run: true
22
logging:
3-
enabled: false
3+
enabled: true
44
level: INFO
55
dir: ~/.local/share/costcutter/logs
66
reporting:
77
csv:
8-
enabled: false
8+
enabled: true
99
path: ~/.local/share/costcutter/reports/events.csv
1010
aws:
1111
profile: default
@@ -20,5 +20,6 @@ aws:
2020
- ap-south-1
2121
services:
2222
# - all to scan for all services (WIP)
23-
- ec2
24-
- lambda
23+
# - ec2
24+
- s3
25+
# - lambda

src/costcutter/orchestrator.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@
88

99
from costcutter.conf.config import get_config
1010
from costcutter.core.session_helper import create_aws_session
11+
from costcutter.reporter import get_reporter
1112

1213
# Reporter no longer needed at service-level (resource handlers still record events)
1314
from costcutter.services.ec2 import cleanup_ec2
15+
from costcutter.services.s3 import cleanup_s3
1416

1517
logger = logging.getLogger(__name__)
1618

1719
SERVICE_HANDLERS = {
1820
# Each value can be a functional entrypoint `run(session, region, dry_run, reporter)`
1921
"ec2": cleanup_ec2,
20-
# "s3": cleanup_s3
22+
"s3": cleanup_s3,
2123
# "lambda": cleanup_lambda,
2224
}
2325

@@ -55,7 +57,7 @@ def process_region_service(
5557

5658
def orchestrate_services(
5759
dry_run: bool = False,
58-
) -> dict[str, int]:
60+
) -> dict[str, Any]:
5961
config = get_config()
6062

6163
# Resolve services
@@ -125,6 +127,9 @@ def orchestrate_services(
125127
total_tasks = max(1, len(tasks))
126128
max_workers = min(32, total_tasks)
127129

130+
# Execute tasks concurrently and track successes/failures
131+
succeeded = 0
132+
failed = 0
128133
with ThreadPoolExecutor(max_workers=max_workers) as executor:
129134
future_map: dict[Any, tuple[str, str]] = {}
130135
for region, service_key, handler_entry in tasks:
@@ -134,6 +139,23 @@ def orchestrate_services(
134139
for future in as_completed(future_map):
135140
region, svc_name = future_map[future]
136141
try:
137-
logger.info("[%s][%s] Task completed", region, svc_name)
142+
# Propagate exceptions from the task so callers can observe failures
143+
future.result()
138144
except Exception as e:
145+
failed += 1
139146
logger.exception("[%s][%s] Task failed: %s", region, svc_name, e)
147+
else:
148+
succeeded += 1
149+
logger.info("[%s][%s] Task completed", region, svc_name)
150+
151+
# After all work is finished, gather recorded events from the global reporter
152+
reporter = get_reporter()
153+
events = reporter.to_dicts()
154+
155+
# Return a small summary including counters and serialized events
156+
return {
157+
"processed": succeeded,
158+
"skipped": skipped,
159+
"failed": failed,
160+
"events": events,
161+
}

src/costcutter/services/ec2/__init__.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
1-
import logging
2-
31
from boto3.session import Session
42

53
from costcutter.services.ec2.instances import cleanup_instances
64
from costcutter.services.ec2.key_pairs import cleanup_key_pairs
75

8-
logger = logging.getLogger(__name__)
9-
_ACCOUNT_ID: str | None = None
106
_HANDLERS = {"instances": cleanup_instances, "key_pairs": cleanup_key_pairs}
117

128

13-
def _get_account_id(session: Session) -> str:
14-
"""Return (and cache) the current AWS account id (simple module cache)."""
15-
global _ACCOUNT_ID
16-
if _ACCOUNT_ID is None:
17-
try:
18-
_ACCOUNT_ID = session.client("sts").get_caller_identity().get("Account", "")
19-
except Exception as e: # pragma: no cover
20-
logger.error("Failed to resolve account id: %s", e)
21-
_ACCOUNT_ID = ""
22-
return _ACCOUNT_ID
23-
24-
259
def cleanup_ec2(session: Session, region: str, dry_run: bool = True, max_workers: int = 1):
2610
# targets: list[str] or None => run all registered
2711
for fn in _HANDLERS.values():
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import logging
2+
3+
from boto3.session import Session
4+
5+
_ACCOUNT_ID: str | None = None
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def _get_account_id(session: Session) -> str:
11+
"""Return (and cache) the current AWS account id (simple module cache)."""
12+
global _ACCOUNT_ID
13+
if _ACCOUNT_ID is None:
14+
try:
15+
_ACCOUNT_ID = session.client("sts").get_caller_identity().get("Account", "")
16+
except Exception as e: # pragma: no cover
17+
logger.error("Failed to resolve account id: %s", e)
18+
_ACCOUNT_ID = ""
19+
return _ACCOUNT_ID

src/costcutter/services/ec2/instances.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from botocore.exceptions import ClientError
77

88
from costcutter.reporter import get_reporter
9-
from costcutter.services.ec2 import _get_account_id
9+
from costcutter.services.ec2.common import _get_account_id
1010

1111
SERVICE: str = "ec2"
1212
RESOURCE: str = "instance"

src/costcutter/services/ec2/key_pairs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from botocore.exceptions import ClientError
66

77
from costcutter.reporter import get_reporter
8-
from costcutter.services.ec2 import _get_account_id
8+
from costcutter.services.ec2.common import _get_account_id
99

1010
SERVICE: str = "ec2"
1111
RESOURCE: str = "key_pair"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from boto3.session import Session
2+
3+
from costcutter.services.s3.buckets import cleanup_buckets
4+
5+
_HANDLERS = {"buckets": cleanup_buckets}
6+
7+
8+
def cleanup_s3(session: Session, region: str, dry_run: bool = True, max_workers: int = 1):
9+
# targets: list[str] or None => run all registered
10+
for fn in _HANDLERS.values():
11+
fn(session=session, region=region, dry_run=dry_run, max_workers=max_workers)

0 commit comments

Comments
 (0)