77import time
88from http import HTTPStatus
99from pathlib import Path
10- from tempfile import NamedTemporaryFile
1110from typing import Optional
1211
1312import docker
1615
1716from samcli .lib .build .utils import _get_host_architecture
1817from samcli .lib .clients .lambda_client import DurableFunctionsClient
19- from samcli .lib .utils .tar import create_tarball
2018from samcli .local .docker .utils import (
21- get_tar_filter_for_windows ,
2219 get_validated_container_client ,
2320 is_image_current ,
2421 to_posix_path ,
@@ -33,8 +30,7 @@ class DurableFunctionsEmulatorContainer:
3330 """
3431
3532 _RAPID_SOURCE_PATH = Path (__file__ ).parent .joinpath (".." , "rapid" ).resolve ()
36- _EMULATOR_IMAGE = "public.ecr.aws/ubuntu/ubuntu:24.04"
37- _EMULATOR_IMAGE_PREFIX = "samcli/durable-execution-emulator"
33+ _EMULATOR_IMAGE_PREFIX = "public.ecr.aws/durable-functions/aws-durable-execution-emulator"
3834 _CONTAINER_NAME = "sam-durable-execution-emulator"
3935 _EMULATOR_DATA_DIR_NAME = ".durable-executions-local"
4036 _EMULATOR_DEFAULT_STORE_TYPE = "sqlite"
@@ -79,11 +75,17 @@ class DurableFunctionsEmulatorContainer:
7975 """
8076 ENV_EMULATOR_PORT = "DURABLE_EXECUTIONS_EMULATOR_PORT"
8177
82- def __init__ (self , container_client = None , existing_container = None ):
78+ """
79+ Allow pinning to a specific emulator image tag/version
80+ """
81+ ENV_EMULATOR_IMAGE_TAG = "DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG"
82+
83+ def __init__ (self , container_client = None , existing_container = None , skip_pull_image = False ):
8384 self ._docker_client_param = container_client
8485 self ._validated_docker_client : Optional [docker .DockerClient ] = None
8586 self .container = existing_container
8687 self .lambda_client : Optional [DurableFunctionsClient ] = None
88+ self ._skip_pull_image = skip_pull_image
8789
8890 self .port = self ._get_emulator_port ()
8991
@@ -137,6 +139,14 @@ def _get_emulator_port(self):
137139 """
138140 return self ._get_port (self .ENV_EXTERNAL_EMULATOR_PORT , self .ENV_EMULATOR_PORT , self .EMULATOR_PORT )
139141
142+ def _get_emulator_image_tag (self ):
143+ """Get the emulator image tag from environment variable or use default."""
144+ return os .environ .get (self .ENV_EMULATOR_IMAGE_TAG , "latest" )
145+
146+ def _get_emulator_image (self ):
147+ """Get the full emulator image name with tag."""
148+ return f"{ self ._EMULATOR_IMAGE_PREFIX } :{ self ._get_emulator_image_tag ()} "
149+
140150 def _get_emulator_store_type (self ):
141151 """Get the store type from environment variable or use default."""
142152 store_type = os .environ .get (self .ENV_STORE_TYPE , self ._EMULATOR_DEFAULT_STORE_TYPE )
@@ -172,15 +182,7 @@ def _get_emulator_environment(self):
172182 Get the environment variables for the emulator container.
173183 """
174184 return {
175- "HOST" : "0.0.0.0" ,
176- "PORT" : str (self .port ),
177- "LOG_LEVEL" : "DEBUG" ,
178- # The emulator needs to have credential variables set, or else it will fail to create boto clients.
179- "AWS_ACCESS_KEY_ID" : "foo" ,
180- "AWS_SECRET_ACCESS_KEY" : "bar" ,
181- "AWS_DEFAULT_REGION" : "us-east-1" ,
182- "EXECUTION_STORE_TYPE" : self ._get_emulator_store_type (),
183- "EXECUTION_TIME_SCALE" : self ._get_emulator_time_scale (),
185+ "DURABLE_EXECUTION_TIME_SCALE" : self ._get_emulator_time_scale (),
184186 }
185187
186188 @property
@@ -198,87 +200,35 @@ def _get_emulator_binary_name(self):
198200 arch = _get_host_architecture ()
199201 return f"aws-durable-execution-emulator-{ arch } "
200202
201- def _generate_emulator_dockerfile (self , emulator_binary_name : str ) -> str :
202- """Generate Dockerfile content for emulator image."""
203- return (
204- f"FROM { self ._EMULATOR_IMAGE } \n "
205- f"COPY { emulator_binary_name } /usr/local/bin/{ emulator_binary_name } \n "
206- f"RUN chmod +x /usr/local/bin/{ emulator_binary_name } \n "
207- )
208-
209- def _get_emulator_image_tag (self , emulator_binary_name : str ) -> str :
210- """Get the Docker image tag for the emulator."""
211- return f"{ self ._EMULATOR_IMAGE_PREFIX } :{ emulator_binary_name } "
212-
213- def _build_emulator_image (self ):
214- """Build Docker image with emulator binary."""
215- emulator_binary_name = self ._get_emulator_binary_name ()
216- binary_path = self ._RAPID_SOURCE_PATH / emulator_binary_name
217-
218- if not binary_path .exists ():
219- raise RuntimeError (f"Durable Functions Emulator binary not found at { binary_path } " )
220-
221- image_tag = self ._get_emulator_image_tag (emulator_binary_name )
222-
223- # Check if image already exists
224- try :
225- self ._docker_client .images .get (image_tag )
226- LOG .debug (f"Emulator image { image_tag } already exists" )
227- return image_tag
228- except docker .errors .ImageNotFound :
229- LOG .debug (f"Building emulator image { image_tag } " )
230-
231- # Generate Dockerfile content
232- dockerfile_content = self ._generate_emulator_dockerfile (emulator_binary_name )
233-
234- # Write Dockerfile to temp location and build image.
235- # Use delete=False because on Windows, NamedTemporaryFile keeps the file
236- # locked while open, preventing tarfile.add() from reading it.
237- dockerfile = NamedTemporaryFile (mode = "w" , suffix = "_Dockerfile" , delete = False )
238- try :
239- dockerfile .write (dockerfile_content )
240- dockerfile .flush ()
241- dockerfile .close ()
242-
243- # Prepare tar paths for build context
244- tar_paths = {
245- dockerfile .name : "Dockerfile" ,
246- str (binary_path ): emulator_binary_name ,
247- }
248-
249- # Use shared tar filter for Windows compatibility
250- tar_filter = get_tar_filter_for_windows ()
251-
252- # Build image using create_tarball utility
253- with create_tarball (tar_paths , tar_filter = tar_filter , dereference = True ) as tarballfile :
254- try :
255- self ._docker_client .images .build (fileobj = tarballfile , custom_context = True , tag = image_tag , rm = True )
256- LOG .info (f"Built emulator image { image_tag } " )
257- return image_tag
258- except Exception as e :
259- raise ClickException (f"Failed to build emulator image: { e } " )
260- finally :
261- os .unlink (dockerfile .name )
262-
263203 def _pull_image_if_needed (self ):
204+ local_image_exists = False
264205 """Pull the emulator image if it doesn't exist locally or is out of date."""
265206 try :
266- self ._docker_client .images .get (self ._EMULATOR_IMAGE )
267- LOG . debug ( f"Emulator image { self . _EMULATOR_IMAGE } exists locally" )
268-
269- if is_image_current (self ._docker_client , self ._EMULATOR_IMAGE ):
207+ self ._docker_client .images .get (self ._get_emulator_image () )
208+ local_image_exists = True
209+ LOG . debug ( f"Emulator image { self . _get_emulator_image () } exists locally" )
210+ if is_image_current (self ._docker_client , self ._get_emulator_image () ):
270211 LOG .debug ("Local emulator image is up-to-date" )
271212 return
272213
273214 LOG .debug ("Local image is out of date and will be updated to the latest version" )
274215 except docker .errors .ImageNotFound :
275- LOG .debug (f"Pulling emulator image { self ._EMULATOR_IMAGE } ..." )
216+ LOG .debug (f"Pulling emulator image { self ._get_emulator_image () } ..." )
276217
277218 try :
278- self ._docker_client .images .pull (self ._EMULATOR_IMAGE )
279- LOG .info (f"Successfully pulled image { self ._EMULATOR_IMAGE } " )
219+ if self ._skip_pull_image and local_image_exists :
220+ LOG .debug ("Skipping pulling new emulator image" )
221+ return
222+ self ._docker_client .images .pull (self ._get_emulator_image ())
223+ LOG .info (f"Successfully pulled image { self ._get_emulator_image ()} " )
280224 except Exception as e :
281- raise ClickException (f"Failed to pull emulator image { self ._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 } " )
282232
283233 def start (self ):
284234 """Start the emulator container."""
@@ -287,8 +237,6 @@ def start(self):
287237 LOG .info ("Using external durable functions emulator, skipping container start" )
288238 return
289239
290- emulator_binary_name = self ._get_emulator_binary_name ()
291-
292240 """
293241 Create persistent volume for execution data to be stored in.
294242 This will be at the current working directory. If a user is running `sam local invoke` in the same
@@ -301,13 +249,27 @@ def start(self):
301249 to_posix_path (emulator_data_dir ): {"bind" : "/tmp/.durable-executions-local" , "mode" : "rw" },
302250 }
303251
304- # Build image with emulator binary
305- image_tag = self ._build_emulator_image ()
252+ self ._pull_image_if_needed ()
306253
307254 LOG .debug (f"Creating container with name={ self ._container_name } , port={ self .port } " )
308255 self .container = self ._docker_client .containers .create (
309- image = image_tag ,
310- command = [f"/usr/local/bin/{ emulator_binary_name } " , "--host" , "0.0.0.0" , "--port" , str (self .port )],
256+ image = self ._get_emulator_image (),
257+ command = [
258+ "dex-local-runner" ,
259+ "start-server" ,
260+ "--host" ,
261+ "0.0.0.0" ,
262+ "--port" ,
263+ str (self .port ),
264+ "--log-level" ,
265+ "DEBUG" ,
266+ "--lambda-endpoint" ,
267+ "http://host.docker.internal:3001" ,
268+ "--store-type" ,
269+ self ._get_emulator_store_type (),
270+ "--store-path" ,
271+ "/tmp/.durable-executions-local/durable-executions.db" , # this is the path within the container
272+ ],
311273 name = self ._container_name ,
312274 ports = {f"{ self .port } /tcp" : self .port },
313275 volumes = volumes ,
@@ -458,4 +420,14 @@ def _wait_for_ready(self, timeout=30):
458420 except Exception :
459421 pass
460422
461- raise RuntimeError (f"Durable Functions Emulator container failed to become ready within { timeout } seconds" )
423+ raise RuntimeError (
424+ f"Durable Functions Emulator container failed to become ready within { timeout } seconds. "
425+ "You may set the DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG env variable to a specific image "
426+ "to ensure that you are using a compatible version. "
427+ f"Check https://${ self ._get_emulator_image ().replace ('public.ecr' , 'gallery.ecr' )} . "
428+ "and https://github.com/aws/aws-durable-execution-sdk-python-testing/releases "
429+ "for valid image tags. If the problems persist, you can try updating the SAM CLI version "
430+ " in case of incompatibility. "
431+ "You may check the emulator_data_dir for the durable-execution-emulator-{timestamp}.log file which "
432+ "contains the emulator logs. This may be useful for debugging."
433+ )
0 commit comments