Skip to content

Commit 7819dd1

Browse files
sodreclaude
andcommitted
🐛 fix(infra): pin LocalStack to 4.14 and remove delete_stack workaround
Pin LocalStack image from :4 (floating) to :4.14 (minor-locked) across CLI, docker-compose.yml, CI workflows, and docs. This resolves issue #81 where LocalStack confused CloudFormation templates during stack deletion. With the fix in place, the defensive try/except around delete_stack() in test teardown is no longer needed and has been removed from all 7 test files. Also fixed the docstring sample output in the `local status` command to reflect the pinned image version. Closes #81 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 49a2c53 commit 7819dd1

12 files changed

Lines changed: 26 additions & 87 deletions

File tree

.github/workflows/ci-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.12"]') || fromJSON('["3.11", "3.12"]') }}
9898
services:
9999
localstack:
100-
image: localstack/localstack:4
100+
image: localstack/localstack:4.14
101101
ports:
102102
- 4566:4566
103103
env:
@@ -160,7 +160,7 @@ jobs:
160160
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.12"]') || fromJSON('["3.11", "3.12"]') }}
161161
services:
162162
localstack:
163-
image: localstack/localstack:4
163+
image: localstack/localstack:4.14
164164
ports:
165165
- 4566:4566
166166
env:
@@ -244,7 +244,7 @@ jobs:
244244
cancel-in-progress: false
245245
services:
246246
localstack:
247-
image: localstack/localstack:4
247+
image: localstack/localstack:4.14
248248
ports:
249249
- 4566:4566
250250
env:

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ name: zae-limiter
1818

1919
services:
2020
localstack:
21-
image: localstack/localstack:4
21+
image: localstack/localstack:4.14
2222
container_name: zae-limiter-localstack
2323
ports:
2424
- "4566:4566"

docs/contributing/localstack.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ LocalStack provides a local AWS environment for development and testing. This gu
5454
-e SERVICES=dynamodb,dynamodbstreams,lambda,cloudformation,logs,iam,cloudwatch,sqs,s3,sts,resourcegroupstaggingapi \
5555
-v /var/run/docker.sock:/var/run/docker.sock \
5656
-v "${TMPDIR:-/tmp}/localstack:/var/lib/localstack" \
57-
localstack/localstack:4
57+
localstack/localstack:4.14
5858
```
5959

6060
!!! important "Docker Socket Required"

src/zae_limiter/local.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _import_docker() -> Any:
4343
# ---------------------------------------------------------------------------
4444

4545
CONTAINER_NAME = "zae-limiter-localstack"
46-
DEFAULT_IMAGE = "localstack/localstack:4"
46+
DEFAULT_IMAGE = "localstack/localstack:4.14"
4747
DEFAULT_PORT = 4566
4848
LOCALSTACK_SERVICES = (
4949
"dynamodb,dynamodbstreams,lambda,cloudformation,"
@@ -440,7 +440,7 @@ def status(docker_host: str | None) -> None:
440440
LocalStack: running
441441
Endpoint: http://localhost:4566
442442
Health: healthy
443-
Image: localstack/localstack:4
443+
Image: localstack/localstack:4.14
444444
Services: dynamodb,dynamodbstreams,lambda,cloudformation,...
445445
446446
To configure your shell:

tests/benchmark/test_aws.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import os
2424
import time
2525
import uuid
26-
import warnings
2726
from concurrent.futures import ThreadPoolExecutor
2827

2928
import pytest
@@ -94,10 +93,7 @@ def aws_benchmark_limiter(self, request, aws_unique_name):
9493
yield limiter
9594

9695
# Cleanup
97-
try:
98-
repo.delete_stack()
99-
except Exception as e:
100-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
96+
repo.delete_stack()
10197

10298
@pytest.mark.benchmark(group="aws-acquire")
10399
def test_acquire_single_limit_aws_latency(self, benchmark, aws_benchmark_limiter):
@@ -232,10 +228,7 @@ def aws_throughput_limiter(self, aws_unique_name):
232228
with limiter:
233229
yield limiter
234230

235-
try:
236-
repo.delete_stack()
237-
except Exception as e:
238-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
231+
repo.delete_stack()
239232

240233
def test_sequential_throughput_aws(self, aws_throughput_limiter):
241234
"""Measure max sequential TPS on real AWS.
@@ -451,10 +444,7 @@ def aws_speculative_stack(self, aws_unique_name):
451444

452445
yield table_name
453446

454-
try:
455-
repo.delete_stack()
456-
except Exception as e:
457-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
447+
repo.delete_stack()
458448

459449
@pytest.fixture(scope="class", params=[False, True], ids=["baseline", "speculative"])
460450
def aws_speculative_limiter(self, request, aws_speculative_stack):
@@ -637,10 +627,7 @@ def aws_cascade_stack(self, aws_unique_name):
637627

638628
yield table_name
639629

640-
try:
641-
repo.delete_stack()
642-
except Exception as e:
643-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
630+
repo.delete_stack()
644631

645632
@pytest.fixture(scope="class")
646633
def aws_cascade_limiter(self, aws_cascade_stack):

tests/e2e/test_aws.py

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,7 @@ async def aws_limiter(self, unique_name_class):
109109
yield limiter
110110

111111
# Clean up stack after test completes
112-
try:
113-
await repo.delete_stack()
114-
except Exception as e:
115-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
112+
await repo.delete_stack()
116113

117114
@pytest.mark.asyncio(loop_scope="class")
118115
async def test_complete_aws_workflow(self, aws_limiter):
@@ -360,10 +357,7 @@ async def aws_limiter_with_snapshots(self, unique_name_class):
360357

361358
yield limiter
362359

363-
try:
364-
await repo.delete_stack()
365-
except Exception as e:
366-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
360+
await repo.delete_stack()
367361

368362
@pytest.mark.asyncio(loop_scope="class")
369363
@pytest.mark.slow
@@ -526,10 +520,7 @@ async def aws_limiter_minimal(self, unique_name_class):
526520

527521
yield limiter
528522

529-
try:
530-
await repo.delete_stack()
531-
except Exception as e:
532-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
523+
await repo.delete_stack()
533524

534525
@pytest.mark.asyncio(loop_scope="class")
535526
async def test_high_throughput_operations(self, aws_limiter_minimal):
@@ -649,10 +640,7 @@ async def aws_limiter_with_tracing(self, unique_name_class):
649640

650641
yield limiter
651642

652-
try:
653-
await repo.delete_stack()
654-
except Exception as e:
655-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
643+
await repo.delete_stack()
656644

657645
@pytest.mark.asyncio(loop_scope="class")
658646
async def test_lambda_tracing_enabled(
@@ -884,10 +872,7 @@ async def aws_limiter_without_tracing(self, unique_name_class):
884872

885873
yield limiter
886874

887-
try:
888-
await repo.delete_stack()
889-
except Exception as e:
890-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
875+
await repo.delete_stack()
891876

892877
@pytest.mark.asyncio(loop_scope="class")
893878
async def test_lambda_tracing_disabled(
@@ -1259,10 +1244,7 @@ async def aws_provisioner_repo(self, unique_name_class):
12591244

12601245
yield repo
12611246

1262-
try:
1263-
await repo.delete_stack()
1264-
except Exception as e:
1265-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
1247+
await repo.delete_stack()
12661248
await repo.close()
12671249

12681250
# NOTE: test_provisioner_cfn_event_lifecycle passes NamespaceId directly.
@@ -1530,10 +1512,7 @@ async def cfn_provisioner_stack(self, unique_name_class):
15301512
except Exception:
15311513
pass # May already be deleted by the test
15321514

1533-
try:
1534-
await repo.delete_stack()
1535-
except Exception as e:
1536-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
1515+
await repo.delete_stack()
15371516
await repo.close()
15381517

15391518
@pytest.mark.asyncio(loop_scope="class")

tests/e2e/test_aws_discovery.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
Resources are cleaned up after tests, but verify via AWS Console.
2020
"""
2121

22-
import warnings
23-
2422
import pytest
2523

2624
from zae_limiter import RateLimiter, Repository, StackOptions
@@ -74,10 +72,7 @@ async def deployed_limiter(self, discovery_name):
7472

7573
yield limiter
7674

77-
try:
78-
await repo.delete_stack()
79-
except Exception as e:
80-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
75+
await repo.delete_stack()
8176

8277
@pytest.mark.asyncio
8378
async def test_stack_has_managed_by_tag(self, deployed_limiter, discovery_name):
@@ -191,10 +186,7 @@ async def tagged_limiter(self, discovery_name):
191186

192187
yield limiter
193188

194-
try:
195-
await repo.delete_stack()
196-
except Exception as e:
197-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
189+
await repo.delete_stack()
198190

199191
@pytest.mark.asyncio
200192
async def test_user_tags_applied_alongside_managed_tags(self, tagged_limiter, discovery_name):

tests/e2e/test_config_storage.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
Note: These tests use minimal stack options (no aggregator) for faster execution.
1818
"""
1919

20-
import warnings
21-
2220
import pytest
2321
import pytest_asyncio # type: ignore[import-untyped]
2422
from click.testing import CliRunner
@@ -343,10 +341,7 @@ def e2e_limiter(self, localstack_endpoint, unique_name_class, minimal_stack_opti
343341

344342
yield limiter
345343

346-
try:
347-
repo.delete_stack()
348-
except Exception as e:
349-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
344+
repo.delete_stack()
350345

351346
def test_resource_config_cli_workflow(self, e2e_limiter, cli_runner, localstack_endpoint):
352347
"""

tests/e2e/test_localstack.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"""
2525

2626
import asyncio
27-
import warnings
2827

2928
import pytest
3029
import pytest_asyncio
@@ -938,10 +937,7 @@ async def test_cloudformation_full_stack_deployment(
938937
assert entity.id == "cfn-full-entity"
939938
assert entity.name == "CFN Full Entity"
940939

941-
try:
942-
await repo.delete_stack()
943-
except Exception as e:
944-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
940+
await repo.delete_stack()
945941

946942
@pytest.mark.asyncio
947943
async def test_cloudformation_aggregator_no_alarms(
@@ -966,10 +962,7 @@ async def test_cloudformation_aggregator_no_alarms(
966962
assert entity.id == "cfn-no-alarms-entity"
967963
assert entity.name == "CFN No Alarms Entity"
968964

969-
try:
970-
await repo.delete_stack()
971-
except Exception as e:
972-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
965+
await repo.delete_stack()
973966

974967

975968
class TestE2ERoleNaming:

tests/fixtures/stacks.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,7 @@ async def create_shared_stack(
102102

103103
async def destroy_shared_stack(repo: Repository) -> None:
104104
"""Delete the CloudFormation stack and close the Repository."""
105-
try:
106-
await repo.delete_stack()
107-
except Exception as e:
108-
warnings.warn(f"Stack cleanup failed: {e}", ResourceWarning, stacklevel=2)
105+
await repo.delete_stack()
109106
await repo.close()
110107

111108

0 commit comments

Comments
 (0)