Skip to content

Commit d3733bf

Browse files
committed
chore: add skip_pull_image support to emulator image + support using local image if remote image pull fails
1 parent 6efae88 commit d3733bf

4 files changed

Lines changed: 54 additions & 6 deletions

File tree

samcli/commands/local/cli_common/durable_context.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ class DurableContext:
1818
Automatically reuses existing running containers when possible.
1919
"""
2020

21-
def __init__(self):
21+
def __init__(self, skip_pull_image=False):
2222
"""
2323
Initialize the durable context.
24+
25+
Parameters
26+
----------
27+
skip_pull_image : bool
28+
If True, skip pulling the emulator container image
2429
"""
2530
self._emulator: Optional[DurableFunctionsEmulatorContainer] = None
2631
self._reused_container = False
32+
self._skip_pull_image = skip_pull_image
2733

2834
def __enter__(self) -> "DurableContext":
2935
"""
3036
Start the emulator container or attach to an already running one
3137
"""
32-
self._emulator = DurableFunctionsEmulatorContainer()
38+
self._emulator = DurableFunctionsEmulatorContainer(skip_pull_image=self._skip_pull_image)
3339
self._reused_container = self._emulator.start_or_attach()
3440
return self
3541

samcli/local/docker/durable_functions_emulator_container.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ class DurableFunctionsEmulatorContainer:
8080
"""
8181
ENV_EMULATOR_IMAGE_TAG = "DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG"
8282

83-
def __init__(self, container_client=None, existing_container=None):
83+
def __init__(self, container_client=None, existing_container=None, skip_pull_image=False):
8484
self._docker_client_param = container_client
8585
self._validated_docker_client: Optional[docker.DockerClient] = None
8686
self.container = existing_container
8787
self.lambda_client: Optional[DurableFunctionsClient] = None
88+
self._skip_pull_image = skip_pull_image
8889

8990
self.port = self._get_emulator_port()
9091

@@ -200,11 +201,12 @@ def _get_emulator_binary_name(self):
200201
return f"aws-durable-execution-emulator-{arch}"
201202

202203
def _pull_image_if_needed(self):
204+
local_image_exists = False
203205
"""Pull the emulator image if it doesn't exist locally or is out of date."""
204206
try:
205207
self._docker_client.images.get(self._get_emulator_image())
208+
local_image_exists = True
206209
LOG.debug(f"Emulator image {self._get_emulator_image()} exists locally")
207-
208210
if is_image_current(self._docker_client, self._get_emulator_image()):
209211
LOG.debug("Local emulator image is up-to-date")
210212
return
@@ -214,10 +216,19 @@ def _pull_image_if_needed(self):
214216
LOG.debug(f"Pulling emulator image {self._get_emulator_image()}...")
215217

216218
try:
219+
if self._skip_pull_image and local_image_exists:
220+
LOG.debug("Skipping pulling new emulator image")
221+
return
217222
self._docker_client.images.pull(self._get_emulator_image())
218223
LOG.info(f"Successfully pulled image {self._get_emulator_image()}")
219224
except Exception as e:
220-
raise ClickException(f"Failed to pull emulator image {self._get_emulator_image()}: {e}")
225+
if local_image_exists:
226+
LOG.debug(
227+
f"Using existing local emulator image since we failed to pull emulator image "
228+
f"{self._get_emulator_image()}: {e}"
229+
)
230+
else:
231+
raise ClickException(f"Failed to pull emulator image {self._get_emulator_image()}: {e}")
221232

222233
def start(self):
223234
"""Start the emulator container."""

samcli/local/lambdafn/runtime.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,9 @@ def get_or_create_emulator_container(self):
477477
DurableFunctionsEmulatorContainer: The singleton emulator container
478478
"""
479479
if self._durable_execution_emulator_container is None:
480-
self._durable_execution_emulator_container = DurableFunctionsEmulatorContainer()
480+
self._durable_execution_emulator_container = DurableFunctionsEmulatorContainer(
481+
skip_pull_image=self._container_manager.skip_pull_image,
482+
)
481483
self._durable_execution_emulator_container.start_or_attach()
482484
LOG.debug("Created and started durable functions emulator container")
483485
return self._durable_execution_emulator_container

tests/unit/local/docker/test_durable_functions_emulator_container.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,20 @@ def test_image_pull_failure_raises_click_exception(self):
421421
container._pull_image_if_needed()
422422
self.assertIn("Failed to pull emulator image", str(context.exception))
423423

424+
@patch("samcli.local.docker.durable_functions_emulator_container.is_image_current")
425+
def test_image_pull_failure_with_existing_local_image_logs_debug_message(self, mock_is_current):
426+
"""Test that image pull failure with existing local image logs debug message before raising exception"""
427+
container = self._create_container()
428+
mock_image = Mock()
429+
self.mock_docker_client.images.get.return_value = mock_image
430+
mock_is_current.return_value = False
431+
self.mock_docker_client.images.pull.side_effect = Exception("Network timeout")
432+
433+
with (self.assertLogs("samcli.local.docker.durable_functions_emulator_container", level="DEBUG") as log,):
434+
container._pull_image_if_needed()
435+
436+
self.assertTrue(any("Using existing local emulator image since we failed to pull" in msg for msg in log.output))
437+
424438
@patch("samcli.local.docker.durable_functions_emulator_container.requests")
425439
def test_start_durable_execution_success(self, mock_requests):
426440
"""Test that start_durable_execution() posts correct payload and returns response json"""
@@ -690,3 +704,18 @@ def test_stop_captures_logs_before_stopping(self):
690704

691705
container._capture_emulator_logs.assert_called_once()
692706
self.mock_container.stop.assert_called_once()
707+
708+
@patch("samcli.local.docker.durable_functions_emulator_container.is_image_current")
709+
def test_skip_pull_image_with_existing_local_image_logs_debug_and_returns_early(self, mock_is_current):
710+
"""Test that _skip_pull_image=True with existing local image logs debug message and returns early"""
711+
container = self._create_container()
712+
container._skip_pull_image = True
713+
mock_image = Mock()
714+
mock_is_current.return_value = False
715+
self.mock_docker_client.images.get.return_value = mock_image
716+
717+
with self.assertLogs("samcli.local.docker.durable_functions_emulator_container", level="DEBUG") as log:
718+
container._pull_image_if_needed()
719+
720+
self.assertTrue(any("Skipping pulling new emulator image" in msg for msg in log.output))
721+
self.mock_docker_client.images.pull.assert_not_called()

0 commit comments

Comments
 (0)