diff --git a/samcli/local/docker/container_client.py b/samcli/local/docker/container_client.py index 6b127802d7..a314ebb943 100644 --- a/samcli/local/docker/container_client.py +++ b/samcli/local/docker/container_client.py @@ -382,7 +382,7 @@ 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 @@ -390,14 +390,15 @@ def is_dockerfile_error(self, error: Union[Exception, str]) -> bool: 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]: diff --git a/samcli/local/docker/image_build_client.py b/samcli/local/docker/image_build_client.py index 71508e2ee1..7d3aa2f3c5 100644 --- a/samcli/local/docker/image_build_client.py +++ b/samcli/local/docker/image_build_client.py @@ -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]]: diff --git a/tests/integration/buildcmd/build_integ_base.py b/tests/integration/buildcmd/build_integ_base.py index 8c4dfa2a06..85fc05576d 100644 --- a/tests/integration/buildcmd/build_integ_base.py +++ b/tests/integration/buildcmd/build_integ_base.py @@ -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"] @@ -139,6 +140,9 @@ def get_command_list( if debug: command_list += ["--debug"] + if use_buildkit: + command_list += ["--use-buildkit"] + if cached: command_list += ["--cached"] diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 66b6d1ae94..28d4d74111 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -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, @@ -46,9 +48,25 @@ @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", @@ -56,7 +74,7 @@ def test_with_invalid_dockerfile_location(self): "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 @@ -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 @@ -86,21 +104,37 @@ 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) @@ -108,7 +142,7 @@ def test_bad_image_archive_fails(self): 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) @@ -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 @@ -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) @@ -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"), [ diff --git a/tests/setup_finch.sh b/tests/setup_finch.sh index 5129284d3e..2a76c82217 100755 --- a/tests/setup_finch.sh +++ b/tests/setup_finch.sh @@ -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 @@ -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 ===" @@ -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