Skip to content

cloud-waste: build AwsCloudClient, the real client, per ADR-0024 - #82

Merged
Nabzx merged 1 commit into
mainfrom
feat/real-aws-client-80
Sep 5, 2026
Merged

Nabzx merged 1 commit into
mainfrom
feat/real-aws-client-80

Conversation

@Nabzx

@Nabzx Nabzx commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes #80.

What

AwsCloudClient, a real boto3-backed implementation of the existing CloudClient Protocol, alongside FakeCloudClient (unchanged, still the default everywhere).

  • Address side unchanged from ADR-0020: describe_addresses/release_address.
  • Instance side: describe_instances, stop_instances, and one batched GetMetricData call per scan (CPUUtilization + NetworkIn + NetworkOut, Average over IDLE_LOOKBACK_DAYS) mapped onto the existing avg_cpu_percent/avg_network_bytes fields - no reshaping of Instance/IdleInstanceAdapter/detector.py, per ADR-0024's locked v1 simplification.
  • Credentials: CloudWasteAwsSettings carries only region/profile_name - boto3's own default credential chain resolves everything else. No project-specific secrets class.
  • Sandbox-first: DryRun gates both mutating calls whenever allow_live=False (always, everywhere in this repo) - confirmed empirically against moto that this matches real AWS's DryRunOperation/InvalidAllocationID.NotFound/InvalidInstanceID.NotFound error codes exactly.
  • Two new client-layer exceptions for adapter.py's classification: LiveActionsDisabledError (permanent - DryRun confirmed it would succeed, but policy never lets it) and TransientCloudError (retryable - an unclassified AWS-side failure). Both dormant in every test/demo/CI path, since FakeCloudClient never raises either.
  • get_released() on a real client can't recover an address's original public_ip once it's truly gone (AWS itself discards it) - returns an honest placeholder; the approval's own Snapshot already recorded the real value durably at proposal time.

Found while building, fixed on the spot

GetMetricData's EndTime landing exactly on datetime.now() can miss the most recent, still-aggregating period's datapoint - confirmed empirically against moto (standing in for CloudWatch's own documented behaviour), fixed with a small buffer.

Testing

moto (ADR-0024 grilled item 2) - never a live account, never real credentials. boto3-stubs/mypy-boto3-ec2/mypy-boto3-cloudwatch added as dev-only type stubs so boto3.client("ec2"/"cloudwatch") resolves to real typed clients under strict mypy.

demo.py wires in AwsCloudClient when CLOUD_WASTE_AWS_REGION is set, FakeCloudClient otherwise (unchanged default - verified the demo's output is byte-identical to before). README documents the least-privilege IAM policy (ec2:DescribeAddresses, ec2:ReleaseAddress, ec2:DescribeInstances, ec2:StopInstances, cloudwatch:GetMetricData) and the opt-in env var; .env.example added.

Verification

  • ruff check / ruff format --check / mypy . - all clean.
  • pytest -v - 71 passed (was 51: +16 AwsCloudClient tests against moto, +4 adapter-level exception-classification tests).
  • uv run python -m cloud_waste.demo - output unchanged from before this PR.
  • Diff scoped entirely to cloud-waste/ - no other package touched.

A real, boto3/moto-backed implementation of the existing CloudClient
Protocol, alongside FakeCloudClient - the address side (describe_addresses/
release_address) unchanged from ADR-0020; the instance side adds
describe_instances/stop_instances and one batched GetMetricData call per
scan (CPUUtilization + NetworkIn + NetworkOut, Average over
IDLE_LOOKBACK_DAYS) mapped onto the existing avg_cpu_percent/
avg_network_bytes fields - no reshaping of Instance/IdleInstanceAdapter/
detector.py, per ADR-0024's locked v1 simplification.

Credentials: CloudWasteAwsSettings carries only region/profile_name -
boto3's own default credential chain resolves everything else (env vars,
a named profile, an IAM role, SSO), never a project-specific secrets
class. DryRun gates both mutating calls whenever allow_live=False (always,
everywhere in this repo) - confirmed empirically against moto that this
matches real AWS's actual DryRunOperation/InvalidAllocationID.NotFound/
InvalidInstanceID.NotFound error codes exactly.

Two new client-layer exceptions classify what adapter.py already needs to:
LiveActionsDisabledError (DryRun confirmed it would succeed, but policy
never lets it - permanent, since the client's own configuration never
changes mid-flight) and TransientCloudError (an unclassified AWS-side
failure - retryable). Both dormant in every test/demo/CI path today, since
FakeCloudClient never raises either.

get_released() on a real client can't recover the original public_ip once
an address is truly gone (AWS itself discards it) - returns an honest
placeholder rather than a fabricated-looking value; the approval's own
Snapshot already recorded the real one durably at proposal time.

Found and fixed empirically, not assumed: GetMetricData's EndTime landing
exactly on datetime.now() can miss the most recent, still-aggregating
period's datapoint - confirmed against moto (standing in for CloudWatch's
own documented behaviour), fixed with a small buffer.

Tested against moto (ADR-0024 grilled item 2), never a live account -
boto3-stubs/mypy-boto3-ec2/mypy-boto3-cloudwatch added as dev-only type
stubs. demo.py wires in AwsCloudClient when CLOUD_WASTE_AWS_REGION is set,
FakeCloudClient otherwise (unchanged default). README documents the
least-privilege IAM policy and the opt-in env var; .env.example added.

71 tests passing (was 51), ruff/mypy clean, demo verified unaffected by
default.

Closes #80
@Nabzx
Nabzx merged commit cf93d92 into main Sep 5, 2026
7 checks passed
@Nabzx
Nabzx deleted the feat/real-aws-client-80 branch September 5, 2026 17:56
Nabzx added a commit that referenced this pull request Sep 5, 2026
ROADMAP's Open decisions section and the README's status section both
still described the real AWS client as pending. Updated both now it's
merged (#82): cloud-waste has a real, boto3-backed CloudClient
implementation alongside the fake one, tested against moto, never
invoked with real credentials by anything in this repo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build AwsCloudClient: the real cloud-waste client, per ADR-0024

1 participant