Skip to content

Commit 1cf01b3

Browse files
authored
feat: Implement Finch container client (#8285)
* feat: Finch Native Support Adds Finch container runtime support as an alternative to Docker for AWS SAM CLI local development and testing. Finch provides a lightweight, open-source container runtime that offers better performance and resource efficiency compared to Docker Desktop, especially inenterprise environments. This change enables SAM CLI users to leverage Finch for local Lambda function development, testing, and debugging without requiring Docker Desktop. How does it address the issue? - Container Runtime Abstraction: Implements a unified container client system that supports both Docker and Finch through a factory pattern - Automatic Detection: Adds enterprise detectionmechanism to automatically identify and configure Finch installations - API Compatibility: Ensures all existing SAM CLI container operations work seamlessly with Finch - Test Infrastructure: Updates integration and unit tests to support both container runtimes - Error Handling: Provides Finch-specific error messages and fallback mechanisms - Performance Optimizations: Implements file locking and build caching to prevent race conditions What side effects does this change have? - Users can now use Finch as a Docker alternative, potentially improving performance and reducing resource usage - Existing Docker workflows remain unchanged; Finch support is additive - New metrics track container runtime preferences for usage analytics * increase time out for build integration tests Issue with AppVeyor Windows instances that take longer to finishing building. * fix: container engine telemetry * fix: container engine telemetry * fix: telemetry test in make pr failure
1 parent 117d537 commit 1cf01b3

File tree

111 files changed

+9072
-800
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+9072
-800
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,4 @@ tests/integration/testdata/sync/code/before/dotnet_function/src/HelloWorld/obj/
423423
# Installer build folder
424424
.build
425425

426+
.kiro

appveyor-ubuntu.yml

Lines changed: 351 additions & 65 deletions
Large diffs are not rendered by default.

samcli/commands/local/cli_common/invoke_context.py

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from samcli.lib.utils.boto_utils import get_boto_client_provider_with_config
2424
from samcli.lib.utils.packagetype import ZIP
2525
from samcli.lib.utils.stream_writer import StreamWriter
26+
from samcli.local.docker.container_client import ContainerClient
2627
from samcli.local.docker.exceptions import PortAlreadyInUse
2728
from samcli.local.docker.lambda_image import LambdaImage
2829
from samcli.local.docker.manager import ContainerManager
@@ -32,12 +33,6 @@
3233
LOG = logging.getLogger(__name__)
3334

3435

35-
class DockerIsNotReachableException(InvokeContextException):
36-
"""
37-
Docker is not installed or not running at the moment
38-
"""
39-
40-
4136
class InvalidEnvironmentVariablesFileException(InvokeContextException):
4237
"""
4338
User provided an environment variables file which couldn't be read by SAM CLI
@@ -220,6 +215,7 @@ def __init__(
220215
self._lambda_runtimes: Optional[Dict[ContainersMode, LambdaRuntime]] = None
221216

222217
self._local_lambda_runner: Optional[LocalLambdaRunner] = None
218+
self._initialized_container_ids: List[str] = []
223219

224220
def __enter__(self) -> "InvokeContext":
225221
"""
@@ -280,14 +276,9 @@ def __enter__(self) -> "InvokeContext":
280276
self._docker_network, self._skip_pull_image, self._shutdown
281277
)
282278

283-
if not self._container_manager.is_docker_reachable:
284-
raise DockerIsNotReachableException(
285-
"Running AWS SAM projects locally requires Docker. Have you got it installed and running?"
286-
)
287-
288279
# initialize all lambda function containers upfront
289280
if self._containers_initializing_mode == ContainersInitializationMode.EAGER:
290-
self._initialize_all_functions_containers()
281+
self._initialized_container_ids = self._initialize_all_functions_containers()
291282

292283
for func in self._function_provider.get_all():
293284
if func.packagetype == ZIP and func.inlinecode:
@@ -312,15 +303,20 @@ def __exit__(self, *args: Any) -> None:
312303
if self._containers_mode == ContainersMode.WARM:
313304
self._clean_running_containers_and_related_resources()
314305

315-
def _initialize_all_functions_containers(self) -> None:
306+
def _initialize_all_functions_containers(self) -> List[str]:
316307
"""
317308
Create and run a container for each available lambda function
309+
310+
Returns:
311+
List[str]: List of container IDs that were created and started
318312
"""
319313
LOG.info("Initializing the lambda functions containers.")
320314

315+
container_ids = []
316+
321317
def initialize_function_container(function: Function) -> None:
322318
function_config = self.local_lambda_runner.get_invoke_config(function)
323-
self.lambda_runtime.run(
319+
container = self.lambda_runtime.run(
324320
container=None,
325321
function_config=function_config,
326322
debug_context=self._debug_context,
@@ -329,13 +325,27 @@ def initialize_function_container(function: Function) -> None:
329325
extra_hosts=self._extra_hosts,
330326
)
331327

328+
# Collect container ID in a thread-safe way
329+
if container and hasattr(container, "id"):
330+
container_ids.append(container.id)
331+
332332
try:
333333
async_context = AsyncContext()
334334
for function in self._function_provider.get_all():
335335
async_context.add_async_task(initialize_function_container, function)
336336

337337
async_context.run_async(default_executor=False)
338-
LOG.info("Containers Initialization is done.")
338+
LOG.info("Containers created. Waiting for readiness...")
339+
LOG.debug("Initialized container IDs: %s", container_ids)
340+
341+
# Wait for all containers to be ready before returning
342+
if container_ids:
343+
self._wait_for_containers_readiness(container_ids)
344+
LOG.info("All containers are ready.")
345+
else:
346+
LOG.info("No containers were created.")
347+
348+
return container_ids
339349
except KeyboardInterrupt:
340350
LOG.debug("Ctrl+C was pressed. Aborting containers initialization")
341351
self._clean_running_containers_and_related_resources()
@@ -347,6 +357,89 @@ def initialize_function_container(function: Function) -> None:
347357
self._clean_running_containers_and_related_resources()
348358
raise ContainersInitializationException("Lambda functions containers initialization failed") from ex
349359

360+
def _wait_for_containers_readiness(self, container_ids: List[str], max_wait_seconds: int = 30) -> None:
361+
"""
362+
Wait for all containers to be running using native Docker container status.
363+
364+
Args:
365+
container_ids: List of container IDs to check
366+
max_wait_seconds: Maximum time to wait for all containers to be running
367+
"""
368+
import time
369+
370+
from samcli.local.docker.utils import get_validated_container_client
371+
372+
if not container_ids:
373+
return
374+
375+
LOG.info("Waiting for %d containers to be running...", len(container_ids))
376+
docker_client = get_validated_container_client()
377+
start_time = time.time()
378+
379+
while time.time() - start_time < max_wait_seconds:
380+
running_containers = 0
381+
382+
for container_id in container_ids:
383+
try:
384+
container = docker_client.containers.get(container_id)
385+
386+
# Check if container is running
387+
if container.status == "running":
388+
running_containers += 1
389+
390+
except Exception as e:
391+
LOG.debug("Error checking container %s: %s", container_id[:12], e)
392+
continue
393+
394+
if running_containers == len(container_ids):
395+
elapsed = time.time() - start_time
396+
LOG.info("All %d containers running after %.1f seconds", len(container_ids), elapsed)
397+
return
398+
399+
# Log progress every 10 seconds
400+
elapsed = time.time() - start_time
401+
if int(elapsed) % 10 == 0 and elapsed > 0:
402+
LOG.info(
403+
"Container status: %d/%d running after %.1f seconds",
404+
running_containers,
405+
len(container_ids),
406+
elapsed,
407+
)
408+
409+
time.sleep(0.5) # Check every 500ms
410+
411+
# Timeout reached
412+
elapsed = time.time() - start_time
413+
running_containers = sum(1 for cid in container_ids if self._is_container_running(docker_client, cid))
414+
415+
if running_containers < len(container_ids):
416+
LOG.warning(
417+
"Container startup timeout after %.1f seconds. %d/%d containers running",
418+
elapsed,
419+
running_containers,
420+
len(container_ids),
421+
)
422+
else:
423+
LOG.info("All containers running after %.1f seconds", elapsed)
424+
425+
def _is_container_running(self, docker_client: ContainerClient, container_id: str) -> bool:
426+
"""
427+
Check if a container is currently running.
428+
429+
Args:
430+
docker_client: Docker client instance
431+
container_id: Container ID to check
432+
433+
Returns:
434+
bool: True if container is running, False otherwise
435+
"""
436+
try:
437+
container = docker_client.containers.get(container_id)
438+
return bool(container.status == "running")
439+
except Exception as e:
440+
LOG.debug("Error checking container %s: %s", container_id[:12], e)
441+
return False
442+
350443
def _clean_running_containers_and_related_resources(self) -> None:
351444
"""
352445
Clean the running containers and any other related open resources,
@@ -453,6 +546,16 @@ def local_lambda_runner(self) -> LocalLambdaRunner:
453546
)
454547
return self._local_lambda_runner
455548

549+
@property
550+
def initialized_container_ids(self) -> List[str]:
551+
"""
552+
Returns the list of container IDs that were initialized in EAGER mode
553+
554+
Returns:
555+
List[str]: List of container IDs, empty if not in EAGER mode or not initialized
556+
"""
557+
return self._initialized_container_ids
558+
456559
@property
457560
def stdout(self) -> StreamWriter:
458561
"""

samcli/commands/logs/command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def do_cli(
154154
Implementation of the ``cli`` method
155155
"""
156156

157-
from datetime import datetime
157+
from datetime import datetime, timezone
158158

159159
from samcli.commands.logs.logs_context import ResourcePhysicalIdResolver, parse_time
160160
from samcli.commands.logs.puller_factory import generate_puller
@@ -168,7 +168,7 @@ def do_cli(
168168
)
169169

170170
sanitized_start_time = parse_time(start_time, "start-time")
171-
sanitized_end_time = parse_time(end_time, "end-time") or datetime.utcnow()
171+
sanitized_end_time = parse_time(end_time, "end-time") or datetime.now(timezone.utc)
172172

173173
boto_client_provider = get_boto_client_provider_with_config(region=region, profile=profile)
174174
boto_resource_provider = get_boto_resource_provider_with_config(region=region, profile=profile)

samcli/commands/package/package_context.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121

2222
import boto3
2323
import click
24-
import docker
2524

2625
from samcli.commands.package.exceptions import PackageFailedError
27-
from samcli.lib.constants import DOCKER_MIN_API_VERSION
2826
from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable
2927
from samcli.lib.package.artifact_exporter import Template
3028
from samcli.lib.package.code_signer import CodeSigner
@@ -36,6 +34,7 @@
3634
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
3735
from samcli.lib.utils.preview_runtimes import PREVIEW_RUNTIMES
3836
from samcli.lib.utils.resources import AWS_LAMBDA_FUNCTION, AWS_SERVERLESS_FUNCTION
37+
from samcli.local.docker.utils import get_validated_container_client
3938
from samcli.yamlhelper import yaml_dump
4039

4140
LOG = logging.getLogger(__name__)
@@ -124,7 +123,7 @@ def run(self):
124123
)
125124
ecr_client = boto3.client("ecr", config=get_boto_config_with_user_agent(region_name=region_name))
126125

127-
docker_client = docker.from_env(version=DOCKER_MIN_API_VERSION)
126+
docker_client = get_validated_container_client()
128127

129128
s3_uploader = S3Uploader(
130129
s3_client, self.s3_bucket, self.s3_prefix, self.kms_key_id, self.force_upload, self.no_progressbar

samcli/commands/sync/sync_context.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
import threading
77
from dataclasses import dataclass
8-
from datetime import datetime
8+
from datetime import datetime, timezone
99
from pathlib import Path
1010
from typing import Dict, Optional, cast
1111

@@ -56,13 +56,13 @@ def update_resource_sync_state(self, resource_id: str, hash_value: str) -> None:
5656
hash_value: str
5757
The logical ID identifier of the resource
5858
"""
59-
self.resource_sync_states[resource_id] = ResourceSyncState(hash_value, datetime.utcnow())
59+
self.resource_sync_states[resource_id] = ResourceSyncState(hash_value, datetime.now(timezone.utc))
6060

6161
def update_infra_sync_time(self) -> None:
6262
"""
6363
Updates the last infra sync time to be stored in the TOML file.
6464
"""
65-
self.latest_infra_sync_time = datetime.utcnow()
65+
self.latest_infra_sync_time = datetime.now(timezone.utc)
6666

6767

6868
def _sync_state_to_toml_document(sync_state: SyncState) -> TOMLDocument:

samcli/commands/traces/command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def do_cli(trace_ids, tailing, start_time, end_time, output, region):
7373
"""
7474
Implementation of the ``cli`` method
7575
"""
76-
from datetime import datetime
76+
from datetime import datetime, timezone
7777

7878
import boto3
7979

@@ -82,7 +82,7 @@ def do_cli(trace_ids, tailing, start_time, end_time, output, region):
8282
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
8383

8484
sanitized_start_time = parse_time(start_time, "start-time")
85-
sanitized_end_time = parse_time(end_time, "end-time") or datetime.utcnow()
85+
sanitized_end_time = parse_time(end_time, "end-time") or datetime.now(timezone.utc)
8686

8787
boto_config = get_boto_config_with_user_agent(region_name=region)
8888
xray_client = boto3.client("xray", config=boto_config)

0 commit comments

Comments
 (0)