Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions samcli/local/docker/container_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,22 +382,23 @@ def is_dockerfile_error(self, error: Union[Exception, str]) -> bool:
Check if error is a dockerfile-related error for Docker.

Docker-specific error patterns for dockerfile-related issues typically
contain "Cannot locate specified Dockerfile" in the error message.
contain "Cannot locate specified Dockerfile" or "failed to read dockerfile" in the error message.

Args:
error: Exception or error message to check

Returns:
bool: True if the error indicates a dockerfile-related issue
"""
patterns = ["Cannot locate specified Dockerfile", "failed to read dockerfile"]
if isinstance(error, docker.errors.APIError):
if not error.is_server_error:
return False
if not hasattr(error, "explanation") or error.explanation is None:
return False
return "Cannot locate specified Dockerfile" in str(error.explanation)
return any(pattern in str(error.explanation) for pattern in patterns)
elif isinstance(error, str):
return "Cannot locate specified Dockerfile" in error
return any(pattern in error for pattern in patterns)
return False

def list_containers_by_image(self, image_name: str, all_containers: bool = True) -> List[Any]:
Expand Down
4 changes: 2 additions & 2 deletions samcli/local/docker/image_build_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ def build_image(
if process.returncode != 0:
raise docker.errors.BuildError(f"Build failed with exit code {process.returncode}", build_log)

for log in build_log:
yield log
# Return a generator that yields the logs
return (log for log in build_log)

@staticmethod
def is_available(engine_type: str) -> Tuple[bool, Optional[str]]:
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/buildcmd/build_integ_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def get_command_list(
config_file=None,
save_params=False,
project_root_dir=None,
use_buildkit=False,
):
command_list = [self.cmd, "build"]

Expand All @@ -139,6 +140,9 @@ def get_command_list(
if debug:
command_list += ["--debug"]

if use_buildkit:
command_list += ["--use-buildkit"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be change in VS Code extension for this feature? If so, should we have a negate version of this as well: --no-use-buildkit. This should be similar to

"--use-container/--no-use-container",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--no-use-buildkit exists in the implementation, is it required to be in the test options as well?


if cached:
command_list += ["--cached"]

Expand Down
131 changes: 122 additions & 9 deletions tests/integration/buildcmd/test_build_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from samcli.lib.utils import osutils
from samcli.local.docker.utils import get_validated_container_client
from samcli.local.docker.image_build_client import CLIBuildClient
from samcli.local.docker.container_client_factory import ContainerClientFactory
from samcli.yamlhelper import yaml_parse
from tests.testing_utils import (
IS_WINDOWS,
Expand Down Expand Up @@ -46,17 +48,33 @@


@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
@parameterized_class(
("use_buildkit",),
[
(False,),
(True,),
],
)
@pytest.mark.filterwarnings("ignore::ResourceWarning")
class TestBuildingImageTypeLambdaDockerFileFailuresContainer(BuildIntegBase):
template = "template_image.yaml"

def setUp(self):
super().setUp()
if self.use_buildkit:
client = ContainerClientFactory.create_client()
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
if not is_available:
self.skipTest(f"Buildkit not available: {error_msg}")

def test_with_invalid_dockerfile_location(self):
overrides = {
"Runtime": "3.10",
"Handler": "handler",
"DockerFile": "ThisDockerfileDoesNotExist",
"Tag": uuid4().hex,
}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
command_result = run_command(cmdlist, cwd=self.working_dir)

# confirm build failed
Expand All @@ -77,7 +95,7 @@ def test_with_invalid_dockerfile_definition(self):
"DockerFile": "InvalidDockerfile",
"Tag": uuid4().hex,
}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
command_result = run_command(cmdlist, cwd=self.working_dir)

# confirm build failed
Expand All @@ -86,29 +104,45 @@ def test_with_invalid_dockerfile_definition(self):


@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
@parameterized_class(
("use_buildkit",),
[
(False,),
(True,),
],
)
@pytest.mark.filterwarnings("ignore::ResourceWarning")
class TestLoadingImagesFromArchiveContainer(BuildIntegBase):
template = "template_loadable_image.yaml"

FUNCTION_LOGICAL_ID = "ImageFunction"

def setUp(self):
super().setUp()
if self.use_buildkit:
client = ContainerClientFactory.create_client()
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
if not is_available:
self.skipTest(f"Buildkit not available: {error_msg}")

def test_load_not_an_archive_passthrough(self):
overrides = {"ImageUri": "./load_image_archive/this_file_does_not_exist.tar.gz"}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
command_result = run_command(cmdlist, cwd=self.working_dir)

self.assertEqual(command_result.process.returncode, 0)

def test_bad_image_archive_fails(self):
overrides = {"ImageUri": "./load_image_archive/error.tar.gz"}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
command_result = run_command(cmdlist, cwd=self.working_dir)

self.assertEqual(command_result.process.returncode, 1)
self.assertIn("unexpected EOF", command_result.stderr.decode())

def test_load_success(self):
overrides = {"ImageUri": "./load_image_archive/archive.tar.gz"}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)
command_result = run_command(cmdlist, cwd=self.working_dir)

self.assertEqual(command_result.process.returncode, 0)
Expand Down Expand Up @@ -139,17 +173,28 @@ def test_load_success(self):
"Skip build tests on windows when running in CI unless overridden",
)
@parameterized_class(
("template", "prop"),
("template", "prop", "use_buildkit"),
[
("template_local_prebuilt_image.yaml", "ImageUri"),
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri"),
("template_local_prebuilt_image.yaml", "ImageUri", False),
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", False),
("template_local_prebuilt_image.yaml", "ImageUri", True),
("template_cfn_local_prebuilt_image.yaml", "Code.ImageUri", True),
],
)
@pytest.mark.filterwarnings("ignore::ResourceWarning")
class TestSkipBuildingFunctionsWithLocalImageUriContainer(BuildIntegBase):
EXPECTED_FILES_PROJECT_MANIFEST: Set[str] = set()

FUNCTION_LOGICAL_ID_IMAGE = "ImageFunction"

def setUp(self):
super().setUp()
if self.use_buildkit:
client = ContainerClientFactory.create_client()
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())
if not is_available:
self.skipTest(f"Buildkit not available: {error_msg}")

@parameterized.expand(["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"])
def test_with_default_requirements(self, runtime):
_tag = uuid4().hex
Expand All @@ -165,7 +210,7 @@ def test_with_default_requirements(self, runtime):
"ImageUri": image_uri,
"Handler": "main.handler",
}
cmdlist = self.get_command_list(parameter_overrides=overrides)
cmdlist = self.get_command_list(parameter_overrides=overrides, use_buildkit=self.use_buildkit)

command_result = run_command(cmdlist, cwd=self.working_dir)
self.assertEqual(command_result.process.returncode, 0)
Expand Down Expand Up @@ -1879,6 +1924,74 @@ def _verify_build_succeeds(self, build_dir):
self.assertIn("BuildImageFunction", build_dir_files)


@skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE)
@parameterized_class(
("cached", "parallel", "use_custom_build_dir"),
[
(False, False, False), # Basic
(True, False, False), # With Caching
(False, True, False), # With parallelism
(False, False, True), # With custom build dir
],
)
@pytest.mark.filterwarnings("ignore::ResourceWarning")
@pytest.mark.tier1
class TestBuildImageWithBuildkit(BuildIntegBase):
"""Test building image functions with buildkit"""

template = "template_image.yaml"
function_logical_id = "ImageFunction"

def test_build_image_function_with_buildkit(self):
client = ContainerClientFactory.create_client()
is_available, error_msg = CLIBuildClient.is_available(client.get_runtime_type())

if not is_available:
self.skipTest(f"Buildkit not available: {error_msg}")

build_dir = self.custom_build_dir if self.use_custom_build_dir else None

tag = uuid4().hex
overrides = {
"Runtime": "3.12",
"Handler": "main.handler",
"DockerFile": "Dockerfile",
"Tag": tag,
}
cmdlist = self.get_command_list(
parameter_overrides=overrides,
use_buildkit=True,
cached=self.cached,
parallel=self.parallel,
build_dir=build_dir,
)

command_result = run_command(cmdlist, cwd=self.working_dir)
self.assertEqual(command_result.process.returncode, 0)

if self.use_custom_build_dir:
built_template = Path(self.custom_build_dir, "template.yaml")
else:
built_template = self.built_template

# Verify image was built
self._verify_image_build_artifact(
built_template,
self.function_logical_id,
"ImageUri",
f"{self.function_logical_id.lower()}:{tag}",
)

# Verify image works
expected = {"pi": "3.14"}
self._verify_invoke_built_function(
built_template,
self.function_logical_id,
self._make_parameter_override_arg(overrides),
expected,
)


@parameterized_class(
("template", "stack_paths", "layer_full_path", "function_full_paths", "invoke_error_message"),
[
Expand Down
23 changes: 14 additions & 9 deletions tests/setup_finch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ sudo apt-get autoremove -y || true

echo "=== Installing Finch ==="
for i in {1..3}; do
if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then
break
fi
sleep 10
if curl -fsSL https://artifact.runfinch.com/deb/GPG_KEY.pub | sudo gpg --dearmor -o /usr/share/keyrings/runfinch-finch-archive-keyring.gpg; then
break
fi
sleep 10
done

echo 'deb [signed-by=/usr/share/keyrings/runfinch-finch-archive-keyring.gpg arch=amd64] https://artifact.runfinch.com/deb noble main' | sudo tee /etc/apt/sources.list.d/runfinch-finch.list
Expand All @@ -31,12 +31,16 @@ sudo systemctl enable --now finch-buildkit
sleep 3
sudo chmod 666 /var/run/finch.sock

echo "=== Configuring finch for non-root access ==="
sudo chmod +s /usr/libexec/finch/nerdctl
sudo chmod +s /usr/bin/finch

echo "=== Waiting for Finch to be ready ==="
for i in {1..12}; do
if sudo finch info >/dev/null 2>&1; then
break
fi
sleep 5
if sudo finch info >/dev/null 2>&1; then
break
fi
sleep 5
done

echo "=== Configuring buildkit sockets ==="
Expand All @@ -52,4 +56,5 @@ sudo finch run --privileged --rm tonistiigi/binfmt:master --install all

echo "=== Finch setup complete ==="
sudo finch info
sudo finch version
# Run finch without sudo here to confirm that it's not required
finch version
Loading