77import time
88from http import HTTPStatus
99from pathlib import Path
10+ from tempfile import NamedTemporaryFile
1011from typing import Optional
1112
1213import docker
1516
1617from samcli .lib .build .utils import _get_host_architecture
1718from samcli .lib .clients .lambda_client import DurableFunctionsClient
19+ from samcli .lib .utils .tar import create_tarball
1820from samcli .local .docker .utils import (
21+ get_tar_filter_for_windows ,
1922 get_validated_container_client ,
2023 is_image_current ,
2124 to_posix_path ,
@@ -30,7 +33,8 @@ class DurableFunctionsEmulatorContainer:
3033 """
3134
3235 _RAPID_SOURCE_PATH = Path (__file__ ).parent .joinpath (".." , "rapid" ).resolve ()
33- _EMULATOR_IMAGE_PREFIX = "public.ecr.aws/durable-functions/aws-durable-execution-emulator"
36+ _EMULATOR_IMAGE = "public.ecr.aws/ubuntu/ubuntu:24.04"
37+ _EMULATOR_IMAGE_PREFIX = "samcli/durable-execution-emulator"
3438 _CONTAINER_NAME = "sam-durable-execution-emulator"
3539 _EMULATOR_DATA_DIR_NAME = ".durable-executions-local"
3640 _EMULATOR_DEFAULT_STORE_TYPE = "sqlite"
@@ -75,17 +79,11 @@ class DurableFunctionsEmulatorContainer:
7579 """
7680 ENV_EMULATOR_PORT = "DURABLE_EXECUTIONS_EMULATOR_PORT"
7781
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 ):
82+ def __init__ (self , container_client = None , existing_container = None ):
8483 self ._docker_client_param = container_client
8584 self ._validated_docker_client : Optional [docker .DockerClient ] = None
8685 self .container = existing_container
8786 self .lambda_client : Optional [DurableFunctionsClient ] = None
88- self ._skip_pull_image = skip_pull_image
8987
9088 self .port = self ._get_emulator_port ()
9189
@@ -139,14 +137,6 @@ def _get_emulator_port(self):
139137 """
140138 return self ._get_port (self .ENV_EXTERNAL_EMULATOR_PORT , self .ENV_EMULATOR_PORT , self .EMULATOR_PORT )
141139
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-
150140 def _get_emulator_store_type (self ):
151141 """Get the store type from environment variable or use default."""
152142 store_type = os .environ .get (self .ENV_STORE_TYPE , self ._EMULATOR_DEFAULT_STORE_TYPE )
@@ -182,7 +172,15 @@ def _get_emulator_environment(self):
182172 Get the environment variables for the emulator container.
183173 """
184174 return {
185- "DURABLE_EXECUTION_TIME_SCALE" : self ._get_emulator_time_scale (),
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 (),
186184 }
187185
188186 @property
@@ -200,35 +198,87 @@ def _get_emulator_binary_name(self):
200198 arch = _get_host_architecture ()
201199 return f"aws-durable-execution-emulator-{ arch } "
202200
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+
203263 def _pull_image_if_needed (self ):
204- local_image_exists = False
205264 """Pull the emulator image if it doesn't exist locally or is out of date."""
206265 try :
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 () ):
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 ):
211270 LOG .debug ("Local emulator image is up-to-date" )
212271 return
213272
214273 LOG .debug ("Local image is out of date and will be updated to the latest version" )
215274 except docker .errors .ImageNotFound :
216- LOG .debug (f"Pulling emulator image { self ._get_emulator_image () } ..." )
275+ LOG .debug (f"Pulling emulator image { self ._EMULATOR_IMAGE } ..." )
217276
218277 try :
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 ()} " )
278+ self ._docker_client .images .pull (self ._EMULATOR_IMAGE )
279+ LOG .info (f"Successfully pulled image { self ._EMULATOR_IMAGE } " )
224280 except Exception as 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 } " )
281+ raise ClickException (f"Failed to pull emulator image { self ._EMULATOR_IMAGE } : { e } " )
232282
233283 def start (self ):
234284 """Start the emulator container."""
@@ -237,6 +287,8 @@ def start(self):
237287 LOG .info ("Using external durable functions emulator, skipping container start" )
238288 return
239289
290+ emulator_binary_name = self ._get_emulator_binary_name ()
291+
240292 """
241293 Create persistent volume for execution data to be stored in.
242294 This will be at the current working directory. If a user is running `sam local invoke` in the same
@@ -249,27 +301,13 @@ def start(self):
249301 to_posix_path (emulator_data_dir ): {"bind" : "/tmp/.durable-executions-local" , "mode" : "rw" },
250302 }
251303
252- self ._pull_image_if_needed ()
304+ # Build image with emulator binary
305+ image_tag = self ._build_emulator_image ()
253306
254307 LOG .debug (f"Creating container with name={ self ._container_name } , port={ self .port } " )
255308 self .container = self ._docker_client .containers .create (
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- ],
309+ image = image_tag ,
310+ command = [f"/usr/local/bin/{ emulator_binary_name } " , "--host" , "0.0.0.0" , "--port" , str (self .port )],
273311 name = self ._container_name ,
274312 ports = {f"{ self .port } /tcp" : self .port },
275313 volumes = volumes ,
@@ -420,14 +458,4 @@ def _wait_for_ready(self, timeout=30):
420458 except Exception :
421459 pass
422460
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- )
461+ raise RuntimeError (f"Durable Functions Emulator container failed to become ready within { timeout } seconds" )
0 commit comments