From e554d6ea0cd7e24b719f8f222810753a6ade9618 Mon Sep 17 00:00:00 2001 From: SageMaker Bot <49924207+sagemaker-bot@users.noreply.github.com> Date: Sun, 31 May 2026 21:47:38 -0700 Subject: [PATCH] fix: Support for docker compose > v2 in local mode (5739) --- .../src/sagemaker/core/local/image.py | 2 +- .../modules/local_core/local_container.py | 2 +- sagemaker-core/tests/unit/local/test_image.py | 14 +++++ .../local_core/test_local_container.py | 53 +++++++++++++++++++ .../sagemaker/train/local/local_container.py | 2 +- .../unit/train/local/test_local_container.py | 24 +++++++++ 6 files changed, 94 insertions(+), 3 deletions(-) diff --git a/sagemaker-core/src/sagemaker/core/local/image.py b/sagemaker-core/src/sagemaker/core/local/image.py index 6da0db50fb..6dbb47ecc7 100644 --- a/sagemaker-core/src/sagemaker/core/local/image.py +++ b/sagemaker-core/src/sagemaker/core/local/image.py @@ -156,7 +156,7 @@ def _get_compose_cmd_prefix(): stderr=subprocess.DEVNULL, encoding="UTF-8", ) - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): logger.info( "'Docker Compose' is not installed. " "Proceeding to check for 'docker-compose' CLI." diff --git a/sagemaker-core/src/sagemaker/core/modules/local_core/local_container.py b/sagemaker-core/src/sagemaker/core/modules/local_core/local_container.py index 06de1cf6ca..cd025b8e39 100644 --- a/sagemaker-core/src/sagemaker/core/modules/local_core/local_container.py +++ b/sagemaker-core/src/sagemaker/core/modules/local_core/local_container.py @@ -611,7 +611,7 @@ def _get_compose_cmd_prefix(self) -> List[str]: stderr=subprocess.DEVNULL, encoding="UTF-8", ) - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): logger.info( "'Docker Compose' is not installed. " "Proceeding to check for 'docker-compose' CLI." diff --git a/sagemaker-core/tests/unit/local/test_image.py b/sagemaker-core/tests/unit/local/test_image.py index 7a7962c19e..0a1dbb4fa3 100644 --- a/sagemaker-core/tests/unit/local/test_image.py +++ b/sagemaker-core/tests/unit/local/test_image.py @@ -550,6 +550,20 @@ def test_get_compose_cmd_prefix_not_installed(self): with pytest.raises(ImportError, match="Docker Compose is not installed"): _SageMakerContainer._get_compose_cmd_prefix() + def test_get_compose_cmd_prefix_docker_binary_not_found_falls_back_to_standalone(self): + """Test _get_compose_cmd_prefix falls back to standalone when docker binary not found""" + with patch("subprocess.check_output", side_effect=FileNotFoundError("No such file or directory: 'docker'")): + with patch("shutil.which", return_value="/usr/local/bin/docker-compose"): + result = _SageMakerContainer._get_compose_cmd_prefix() + assert result == ["docker-compose"] + + def test_get_compose_cmd_prefix_docker_binary_not_found_no_standalone_raises(self): + """Test _get_compose_cmd_prefix raises when docker binary not found and no standalone""" + with patch("subprocess.check_output", side_effect=FileNotFoundError("No such file or directory: 'docker'")): + with patch("shutil.which", return_value=None): + with pytest.raises(ImportError, match="Docker Compose is not installed"): + _SageMakerContainer._get_compose_cmd_prefix() + def test_process_with_multiple_inputs(self, mock_session): """Test process method with multiple processing inputs""" container = _SageMakerContainer( diff --git a/sagemaker-core/tests/unit/modules/local_core/test_local_container.py b/sagemaker-core/tests/unit/modules/local_core/test_local_container.py index a4c137484d..f3ad8d464d 100644 --- a/sagemaker-core/tests/unit/modules/local_core/test_local_container.py +++ b/sagemaker-core/tests/unit/modules/local_core/test_local_container.py @@ -714,6 +714,59 @@ def test_get_compose_cmd_prefix_docker_compose_v1_rejected( with pytest.raises(ImportError, match="Docker Compose is not installed"): container._get_compose_cmd_prefix() + @patch("sagemaker.core.modules.local_core.local_container.subprocess.check_output") + @patch("sagemaker.core.modules.local_core.local_container.shutil.which") + def test_get_compose_cmd_prefix_docker_binary_not_found_falls_back_to_standalone( + self, mock_which, mock_check_output, mock_session, basic_channel + ): + """Test _get_compose_cmd_prefix falls back to standalone when docker binary not found""" + container = _LocalContainer( + training_job_name="test-job", + instance_type="local", + instance_count=1, + image="test-image:latest", + container_root="/tmp/test", + input_data_config=[basic_channel], + environment={}, + hyper_parameters={}, + container_entrypoint=[], + container_arguments=[], + sagemaker_session=mock_session, + ) + + mock_check_output.side_effect = FileNotFoundError("No such file or directory: 'docker'") + mock_which.return_value = "/usr/local/bin/docker-compose" + + result = container._get_compose_cmd_prefix() + + assert result == ["docker-compose"] + + @patch("sagemaker.core.modules.local_core.local_container.subprocess.check_output") + @patch("sagemaker.core.modules.local_core.local_container.shutil.which") + def test_get_compose_cmd_prefix_docker_binary_not_found_no_standalone_raises( + self, mock_which, mock_check_output, mock_session, basic_channel + ): + """Test _get_compose_cmd_prefix raises when docker binary not found and no standalone""" + container = _LocalContainer( + training_job_name="test-job", + instance_type="local", + instance_count=1, + image="test-image:latest", + container_root="/tmp/test", + input_data_config=[basic_channel], + environment={}, + hyper_parameters={}, + container_entrypoint=[], + container_arguments=[], + sagemaker_session=mock_session, + ) + + mock_check_output.side_effect = FileNotFoundError("No such file or directory: 'docker'") + mock_which.return_value = None + + with pytest.raises(ImportError, match="Docker Compose is not installed"): + container._get_compose_cmd_prefix() + def test_init_with_container_entrypoint(self, mock_session, basic_channel): """Test initialization with container entrypoint""" container = _LocalContainer( diff --git a/sagemaker-train/src/sagemaker/train/local/local_container.py b/sagemaker-train/src/sagemaker/train/local/local_container.py index 26f9a62e9f..3f721fab93 100644 --- a/sagemaker-train/src/sagemaker/train/local/local_container.py +++ b/sagemaker-train/src/sagemaker/train/local/local_container.py @@ -619,7 +619,7 @@ def _get_compose_cmd_prefix(self) -> List[str]: stderr=subprocess.DEVNULL, encoding="UTF-8", ) - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): logger.info( "'Docker Compose' is not installed. " "Proceeding to check for 'docker-compose' CLI." diff --git a/sagemaker-train/tests/unit/train/local/test_local_container.py b/sagemaker-train/tests/unit/train/local/test_local_container.py index 7393b6c6f0..650c3f9920 100644 --- a/sagemaker-train/tests/unit/train/local/test_local_container.py +++ b/sagemaker-train/tests/unit/train/local/test_local_container.py @@ -191,3 +191,27 @@ def test_get_compose_cmd_prefix_standalone_fallback( mock_which.return_value = "/usr/local/bin/docker-compose" result = container._get_compose_cmd_prefix() assert result == ["docker-compose"] + + @patch("sagemaker.train.local.local_container.shutil.which") + @patch("sagemaker.train.local.local_container.subprocess.check_output") + def test_get_compose_cmd_prefix_docker_binary_not_found_falls_back_to_standalone( + self, mock_check_output, mock_which, _basic_channel + ): + """When docker binary is not found (FileNotFoundError), falls back to docker-compose standalone.""" + container = _make_container(_basic_channel) + mock_check_output.side_effect = FileNotFoundError("No such file or directory: 'docker'") + mock_which.return_value = "/usr/local/bin/docker-compose" + result = container._get_compose_cmd_prefix() + assert result == ["docker-compose"] + + @patch("sagemaker.train.local.local_container.shutil.which") + @patch("sagemaker.train.local.local_container.subprocess.check_output") + def test_get_compose_cmd_prefix_docker_binary_not_found_no_standalone_raises( + self, mock_check_output, mock_which, _basic_channel + ): + """When docker binary is not found and no standalone docker-compose, raises ImportError.""" + container = _make_container(_basic_channel) + mock_check_output.side_effect = FileNotFoundError("No such file or directory: 'docker'") + mock_which.return_value = None + with pytest.raises(ImportError, match="Docker Compose is not installed"): + container._get_compose_cmd_prefix()