-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Feat/use durable functions emulator image #8708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
7eca282
35f322c
58d4e91
a0045a0
ea7a340
1430958
9452dab
2a2469c
681ae2d
3b0f0c9
384b9fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,6 @@ | |
| import time | ||
| from http import HTTPStatus | ||
| from pathlib import Path | ||
| from tempfile import NamedTemporaryFile | ||
| from typing import Optional | ||
|
|
||
| import docker | ||
|
|
@@ -16,8 +15,7 @@ | |
|
|
||
| from samcli.lib.build.utils import _get_host_architecture | ||
| from samcli.lib.clients.lambda_client import DurableFunctionsClient | ||
| from samcli.lib.utils.tar import create_tarball | ||
| from samcli.local.docker.utils import get_tar_filter_for_windows, get_validated_container_client, is_image_current | ||
| from samcli.local.docker.utils import get_validated_container_client, is_image_current | ||
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -28,8 +26,7 @@ class DurableFunctionsEmulatorContainer: | |
| """ | ||
|
|
||
| _RAPID_SOURCE_PATH = Path(__file__).parent.joinpath("..", "rapid").resolve() | ||
| _EMULATOR_IMAGE = "public.ecr.aws/ubuntu/ubuntu:24.04" | ||
| _EMULATOR_IMAGE_PREFIX = "samcli/durable-execution-emulator" | ||
| _EMULATOR_IMAGE_PREFIX = "public.ecr.aws/o4w4w0v6/aws-durable-execution-emulator" | ||
| _CONTAINER_NAME = "sam-durable-execution-emulator" | ||
| _EMULATOR_DATA_DIR_NAME = ".durable-executions-local" | ||
| _EMULATOR_DEFAULT_STORE_TYPE = "sqlite" | ||
|
|
@@ -74,6 +71,11 @@ class DurableFunctionsEmulatorContainer: | |
| """ | ||
| ENV_EMULATOR_PORT = "DURABLE_EXECUTIONS_EMULATOR_PORT" | ||
|
|
||
| """ | ||
| Allow pinning to a specific emulator image tag/version | ||
| """ | ||
| ENV_EMULATOR_IMAGE_TAG = "DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG" | ||
|
|
||
| def __init__(self, container_client=None, existing_container=None): | ||
| self._docker_client_param = container_client | ||
| self._validated_docker_client: Optional[docker.DockerClient] = None | ||
|
|
@@ -132,6 +134,14 @@ def _get_emulator_port(self): | |
| """ | ||
| return self._get_port(self.ENV_EXTERNAL_EMULATOR_PORT, self.ENV_EMULATOR_PORT, self.EMULATOR_PORT) | ||
|
|
||
| def _get_emulator_image_tag(self): | ||
| """Get the emulator image tag from environment variable or use default.""" | ||
| return os.environ.get(self.ENV_EMULATOR_IMAGE_TAG, "latest") | ||
|
|
||
| def _get_emulator_image(self): | ||
| """Get the full emulator image name with tag.""" | ||
| return f"{self._EMULATOR_IMAGE_PREFIX}:{self._get_emulator_image_tag()}" | ||
|
|
||
| def _get_emulator_store_type(self): | ||
| """Get the store type from environment variable or use default.""" | ||
| store_type = os.environ.get(self.ENV_STORE_TYPE, self._EMULATOR_DEFAULT_STORE_TYPE) | ||
|
|
@@ -167,15 +177,11 @@ def _get_emulator_environment(self): | |
| Get the environment variables for the emulator container. | ||
| """ | ||
| return { | ||
| "HOST": "0.0.0.0", | ||
| "PORT": str(self.port), | ||
| "LOG_LEVEL": "DEBUG", | ||
| # The emulator needs to have credential variables set, or else it will fail to create boto clients. | ||
| "AWS_ACCESS_KEY_ID": "foo", | ||
| "AWS_SECRET_ACCESS_KEY": "bar", | ||
| "AWS_DEFAULT_REGION": "us-east-1", | ||
| "EXECUTION_STORE_TYPE": self._get_emulator_store_type(), | ||
| "EXECUTION_TIME_SCALE": self._get_emulator_time_scale(), | ||
| "DURABLE_EXECUTION_TIME_SCALE": self._get_emulator_time_scale(), | ||
| } | ||
|
|
||
| @property | ||
|
|
@@ -193,81 +199,25 @@ def _get_emulator_binary_name(self): | |
| arch = _get_host_architecture() | ||
| return f"aws-durable-execution-emulator-{arch}" | ||
|
|
||
| def _generate_emulator_dockerfile(self, emulator_binary_name: str) -> str: | ||
| """Generate Dockerfile content for emulator image.""" | ||
| return ( | ||
| f"FROM {self._EMULATOR_IMAGE}\n" | ||
| f"COPY {emulator_binary_name} /usr/local/bin/{emulator_binary_name}\n" | ||
| f"RUN chmod +x /usr/local/bin/{emulator_binary_name}\n" | ||
| ) | ||
|
|
||
| def _get_emulator_image_tag(self, emulator_binary_name: str) -> str: | ||
| """Get the Docker image tag for the emulator.""" | ||
| return f"{self._EMULATOR_IMAGE_PREFIX}:{emulator_binary_name}" | ||
|
|
||
| def _build_emulator_image(self): | ||
| """Build Docker image with emulator binary.""" | ||
| emulator_binary_name = self._get_emulator_binary_name() | ||
| binary_path = self._RAPID_SOURCE_PATH / emulator_binary_name | ||
|
|
||
| if not binary_path.exists(): | ||
| raise RuntimeError(f"Durable Functions Emulator binary not found at {binary_path}") | ||
|
|
||
| image_tag = self._get_emulator_image_tag(emulator_binary_name) | ||
|
|
||
| # Check if image already exists | ||
| try: | ||
| self._docker_client.images.get(image_tag) | ||
| LOG.debug(f"Emulator image {image_tag} already exists") | ||
| return image_tag | ||
| except docker.errors.ImageNotFound: | ||
| LOG.debug(f"Building emulator image {image_tag}") | ||
|
|
||
| # Generate Dockerfile content | ||
| dockerfile_content = self._generate_emulator_dockerfile(emulator_binary_name) | ||
|
|
||
| # Write Dockerfile to temp location and build image | ||
| with NamedTemporaryFile(mode="w", suffix="_Dockerfile") as dockerfile: | ||
| dockerfile.write(dockerfile_content) | ||
| dockerfile.flush() | ||
|
|
||
| # Prepare tar paths for build context | ||
| tar_paths = { | ||
| dockerfile.name: "Dockerfile", | ||
| str(binary_path): emulator_binary_name, | ||
| } | ||
|
|
||
| # Use shared tar filter for Windows compatibility | ||
| tar_filter = get_tar_filter_for_windows() | ||
|
|
||
| # Build image using create_tarball utility | ||
| with create_tarball(tar_paths, tar_filter=tar_filter, dereference=True) as tarballfile: | ||
| try: | ||
| self._docker_client.images.build(fileobj=tarballfile, custom_context=True, tag=image_tag, rm=True) | ||
| LOG.info(f"Built emulator image {image_tag}") | ||
| return image_tag | ||
| except Exception as e: | ||
| raise ClickException(f"Failed to build emulator image: {e}") | ||
|
|
||
| def _pull_image_if_needed(self): | ||
| """Pull the emulator image if it doesn't exist locally or is out of date.""" | ||
| try: | ||
| self._docker_client.images.get(self._EMULATOR_IMAGE) | ||
| LOG.debug(f"Emulator image {self._EMULATOR_IMAGE} exists locally") | ||
| self._docker_client.images.get(self._get_emulator_image()) | ||
| LOG.debug(f"Emulator image {self._get_emulator_image()} exists locally") | ||
|
|
||
| if is_image_current(self._docker_client, self._EMULATOR_IMAGE): | ||
| if is_image_current(self._docker_client, self._get_emulator_image()): | ||
| LOG.debug("Local emulator image is up-to-date") | ||
| return | ||
|
|
||
| LOG.debug("Local image is out of date and will be updated to the latest version") | ||
| except docker.errors.ImageNotFound: | ||
| LOG.debug(f"Pulling emulator image {self._EMULATOR_IMAGE}...") | ||
| LOG.debug(f"Pulling emulator image {self._get_emulator_image()}...") | ||
|
|
||
| try: | ||
| self._docker_client.images.pull(self._EMULATOR_IMAGE) | ||
| LOG.info(f"Successfully pulled image {self._EMULATOR_IMAGE}") | ||
| self._docker_client.images.pull(self._get_emulator_image()) | ||
| LOG.info(f"Successfully pulled image {self._get_emulator_image()}") | ||
| except Exception as e: | ||
| raise ClickException(f"Failed to pull emulator image {self._EMULATOR_IMAGE}: {e}") | ||
| raise ClickException(f"Failed to pull emulator image {self._get_emulator_image()}: {e}") | ||
|
|
||
| def start(self): | ||
| """Start the emulator container.""" | ||
|
|
@@ -276,8 +226,6 @@ def start(self): | |
| LOG.info("Using external durable functions emulator, skipping container start") | ||
| return | ||
|
|
||
| emulator_binary_name = self._get_emulator_binary_name() | ||
|
|
||
| """ | ||
| Create persistent volume for execution data to be stored in. | ||
| This will be at the current working directory. If a user is running `sam local invoke` in the same | ||
|
|
@@ -290,13 +238,27 @@ def start(self): | |
| emulator_data_dir: {"bind": "/tmp/.durable-executions-local", "mode": "rw"}, | ||
| } | ||
|
|
||
| # Build image with emulator binary | ||
| image_tag = self._build_emulator_image() | ||
| self._pull_image_if_needed() | ||
|
|
||
| LOG.debug(f"Creating container with name={self._container_name}, port={self.port}") | ||
| self.container = self._docker_client.containers.create( | ||
| image=image_tag, | ||
| command=[f"/usr/local/bin/{emulator_binary_name}", "--host", "0.0.0.0", "--port", str(self.port)], | ||
| image=self._get_emulator_image(), | ||
| command=[ | ||
| "dex-local-runner", | ||
| "start-server", | ||
| "--host", | ||
| "0.0.0.0", | ||
| "--port", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess not related to this PR, but do we have plan to assign this port dynamically. Currently it would just use 9014 no matter it's taken or not
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh really, I thought the code already dynamically assigned the port according to the env variable.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. humm, which part set this envvar, I might have missed that |
||
| str(self.port), | ||
| "--log-level", | ||
| "DEBUG", | ||
| "--lambda-endpoint", | ||
| "http://host.docker.internal:3001", | ||
| "--store-type", | ||
| self._get_emulator_store_type(), | ||
|
Comment on lines
+257
to
+258
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In line 187 this is already added to the environment variables, so we probably don't need to add it here again.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, EXECUTION_STORE_TYPE is part of the emulator "layer" that was removed. So it's not being used anymore. I can remove it from the environment variables.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. conversely, I can also use the "new" environment variables instead:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. I see. I think both alternatives work, as long as it's clear. I think it makes sense to keep it here and remove it from the env vars.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so only self.port - Can be overridden by user so maybe: |
||
| "--store-path", | ||
| "/tmp/.durable-executions-local/durable-executions.db", | ||
| ], | ||
| name=self._container_name, | ||
| ports={f"{self.port}/tcp": self.port}, | ||
| volumes=volumes, | ||
|
|
@@ -447,4 +409,12 @@ def _wait_for_ready(self, timeout=30): | |
| except Exception: | ||
| pass | ||
|
|
||
| raise RuntimeError(f"Durable Functions Emulator container failed to become ready within {timeout} seconds") | ||
| raise RuntimeError( | ||
| f"Durable Functions Emulator container failed to become ready within {timeout} seconds. " | ||
| "You may set the DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG env variable to a specific image " | ||
| "to ensure that you are using a compatible version. " | ||
| f"Check https://${self._get_emulator_image().replace('public.ecr', 'gallery.ecr')}. " | ||
| "and https://github.com/aws/aws-durable-execution-sdk-python-testing/releases " | ||
| "for valid image tags. If the problems persist, you can try updating the SAM CLI version " | ||
| " in case of incompatibility." | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sam local invokehas a --invoke-image parameter, where customers can pass the location of a specific image to be used as the execution base image instead of the default Lambda base image.https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-invoke.html#ref-sam-cli-local-invoke-options-invoke-image
could explore using this established pattern rather than adding ENV_EMULATOR_IMAGE_TAG?