Skip to content

Commit 4c0fb3c

Browse files
feat: Local Mode - Add Support for Docker Compose V2 (#4111)
1 parent 1602823 commit 4c0fb3c

File tree

4 files changed

+104
-19
lines changed

4 files changed

+104
-19
lines changed

doc/overview.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,14 +1550,15 @@ them to your local environment. This is a great way to test your deep learning s
15501550
managed training or hosting environments. Local Mode is supported for frameworks images (TensorFlow, MXNet, Chainer, PyTorch,
15511551
and Scikit-Learn) and images you supply yourself.
15521552

1553-
You can install necessary dependencies for this feature using pip; local mode also requires docker-compose which you can
1554-
install using the following steps (More info - https://github.com/docker/compose#where-to-get-docker-compose ):
1553+
You can install necessary dependencies for this feature using pip.
15551554

15561555
::
15571556

15581557
pip install 'sagemaker[local]' --upgrade
1559-
curl -L "https://github.com/docker/compose/releases/download/v2.7.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
1560-
chmod +x /usr/local/bin/docker-compose
1558+
1559+
1560+
Additionally, Local Mode also requires Docker Compose V2. Follow the guidelines in https://docs.docker.com/compose/install/ to install.
1561+
Make sure to have a Compose Version compatible with your Docker Engine installation. Check Docker Engine release notes https://docs.docker.com/engine/release-notes to find a compatible version.
15611562

15621563
If you want to keep everything local, and not use Amazon S3 either, you can enable "local code" in one of two ways:
15631564

src/sagemaker/local/image.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,8 @@ def __init__(
9494
from sagemaker.local.local_session import LocalSession
9595

9696
# check if docker-compose is installed
97-
if find_executable("docker-compose") is None:
98-
raise ImportError(
99-
"'docker-compose' is not installed. "
100-
"Local Mode features will not work without docker-compose. "
101-
"For more information on how to install 'docker-compose', please, see "
102-
"https://docs.docker.com/compose/install/"
103-
)
10497

98+
self.compose_cmd_prefix = _SageMakerContainer._get_compose_cmd_prefix()
10599
self.sagemaker_session = sagemaker_session or LocalSession()
106100
self.instance_type = instance_type
107101
self.instance_count = instance_count
@@ -118,6 +112,51 @@ def __init__(
118112
self.container_root = None
119113
self.container = None
120114

115+
@staticmethod
116+
def _get_compose_cmd_prefix():
117+
"""Gets the Docker Compose command.
118+
119+
The method initially looks for 'docker compose' v2
120+
executable, if not found looks for 'docker-compose' executable.
121+
122+
Returns:
123+
Docker Compose executable split into list.
124+
125+
Raises:
126+
ImportError: If Docker Compose executable was not found.
127+
"""
128+
compose_cmd_prefix = []
129+
130+
output = None
131+
try:
132+
output = subprocess.check_output(
133+
["docker", "compose", "version"],
134+
stderr=subprocess.DEVNULL,
135+
encoding="UTF-8",
136+
)
137+
except subprocess.CalledProcessError:
138+
logger.info(
139+
"'Docker Compose' is not installed. "
140+
"Proceeding to check for 'docker-compose' CLI."
141+
)
142+
143+
if output and "v2" in output.strip():
144+
logger.info("'Docker Compose' found using Docker CLI.")
145+
compose_cmd_prefix.extend(["docker", "compose"])
146+
return compose_cmd_prefix
147+
148+
if find_executable("docker-compose") is not None:
149+
logger.info("'Docker Compose' found using Docker Compose CLI.")
150+
compose_cmd_prefix.extend(["docker-compose"])
151+
return compose_cmd_prefix
152+
153+
raise ImportError(
154+
"Docker Compose is not installed. "
155+
"Local Mode features will not work without docker compose. "
156+
"For more information on how to install 'docker compose', please, see "
157+
"https://docs.docker.com/compose/install/"
158+
)
159+
121160
def process(
122161
self,
123162
processing_inputs,
@@ -715,19 +754,20 @@ def _compose(self, detached=False):
715754
Args:
716755
detached:
717756
"""
718-
compose_cmd = "docker-compose"
757+
compose_cmd = self.compose_cmd_prefix
719758

720759
command = [
721-
compose_cmd,
722760
"-f",
723761
os.path.join(self.container_root, DOCKER_COMPOSE_FILENAME),
724762
"up",
725763
"--build",
726764
"--abort-on-container-exit" if not detached else "--detach", # mutually exclusive
727765
]
728766

729-
logger.info("docker command: %s", " ".join(command))
730-
return command
767+
compose_cmd.extend(command)
768+
769+
logger.info("docker command: %s", " ".join(compose_cmd))
770+
return compose_cmd
731771

732772
def _create_docker_host(self, host, environment, optml_subdirs, command, volumes):
733773
"""Creates the docker host configuration.

tests/scripts/run-notebook-test.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ echo "set SAGEMAKER_ROLE_ARN=$SAGEMAKER_ROLE_ARN"
141141
./amazon-sagemaker-examples/sagemaker-python-sdk/scikit_learn_randomforest/Sklearn_on_SageMaker_end2end.ipynb \
142142
./amazon-sagemaker-examples/sagemaker-pipelines/tabular/abalone_build_train_deploy/sagemaker-pipelines-preprocess-train-evaluate-batch-transform.ipynb \
143143
144-
# Skipping test until fix in example notebook to move to new conda environment
145-
#./amazon-sagemaker-examples/advanced_functionality/kmeans_bring_your_own_model/kmeans_bring_your_own_model.ipynb \
146-
147144
# Skipping test until fix in example notebook to install docker-compose is complete
148145
#./amazon-sagemaker-examples/sagemaker-python-sdk/tensorflow_moving_from_framework_mode_to_script_mode/tensorflow_moving_from_framework_mode_to_script_mode.ipynb \
149146

tests/unit/sagemaker/local/test_local_image.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import pytest
2929
import yaml
3030
from mock import patch, Mock, MagicMock
31-
3231
import sagemaker
3332
from sagemaker.local.image import _SageMakerContainer, _Volume, _aws_credentials
3433

@@ -91,6 +90,38 @@ def sagemaker_session():
9190
return sms
9291

9392

93+
@patch("subprocess.check_output", Mock(return_value="Docker Compose version v2.0.0-rc.3"))
94+
def test_get_compose_cmd_prefix_with_docker_cli():
95+
compose_cmd_prefix = _SageMakerContainer._get_compose_cmd_prefix()
96+
assert compose_cmd_prefix == ["docker", "compose"]
97+
98+
99+
@patch(
100+
"subprocess.check_output",
101+
side_effect=subprocess.CalledProcessError(returncode=1, cmd="docker compose version"),
102+
)
103+
@patch("sagemaker.local.image.find_executable", Mock(return_value="/usr/bin/docker-compose"))
104+
def test_get_compose_cmd_prefix_with_docker_compose_cli(check_output):
105+
compose_cmd_prefix = _SageMakerContainer._get_compose_cmd_prefix()
106+
assert compose_cmd_prefix == ["docker-compose"]
107+
108+
109+
@patch(
110+
"subprocess.check_output",
111+
side_effect=subprocess.CalledProcessError(returncode=1, cmd="docker compose version"),
112+
)
113+
@patch("sagemaker.local.image.find_executable", Mock(return_value=None))
114+
def test_get_compose_cmd_prefix_raises_import_error(check_output):
115+
with pytest.raises(ImportError) as e:
116+
_SageMakerContainer._get_compose_cmd_prefix()
117+
assert (
118+
"Docker Compose is not installed. "
119+
"Local Mode features will not work without docker compose. "
120+
"For more information on how to install 'docker compose', please, see "
121+
"https://docs.docker.com/compose/install/" in str(e)
122+
)
123+
124+
94125
def test_sagemaker_container_hosts_should_have_lowercase_names():
95126
random.seed(a=42)
96127

@@ -333,6 +364,10 @@ def test_check_output():
333364
@patch("sagemaker.local.image._stream_output", Mock())
334365
@patch("sagemaker.local.image._SageMakerContainer._cleanup")
335366
@patch("sagemaker.local.image._SageMakerContainer.retrieve_artifacts")
367+
@patch(
368+
"sagemaker.local.image._SageMakerContainer._get_compose_cmd_prefix",
369+
Mock(return_value=["docker-compose"]),
370+
)
336371
@patch("sagemaker.local.data.get_data_source_instance")
337372
@patch("subprocess.Popen")
338373
def test_train(
@@ -438,6 +473,10 @@ def test_train_with_hyperparameters_without_job_name(
438473
@patch("sagemaker.local.image._stream_output", side_effect=RuntimeError("this is expected"))
439474
@patch("sagemaker.local.image._SageMakerContainer._cleanup")
440475
@patch("sagemaker.local.image._SageMakerContainer.retrieve_artifacts")
476+
@patch(
477+
"sagemaker.local.image._SageMakerContainer._get_compose_cmd_prefix",
478+
Mock(return_value=["docker-compose"]),
479+
)
441480
@patch("sagemaker.local.data.get_data_source_instance")
442481
@patch("subprocess.Popen", Mock())
443482
def test_train_error(
@@ -475,6 +514,10 @@ def test_train_error(
475514
@patch("sagemaker.local.local_session.LocalSession", Mock())
476515
@patch("sagemaker.local.image._stream_output", Mock())
477516
@patch("sagemaker.local.image._SageMakerContainer._cleanup", Mock())
517+
@patch(
518+
"sagemaker.local.image._SageMakerContainer._get_compose_cmd_prefix",
519+
Mock(return_value=["docker-compose"]),
520+
)
478521
@patch("sagemaker.local.data.get_data_source_instance")
479522
@patch("subprocess.Popen", Mock())
480523
def test_train_local_code(get_data_source_instance, tmpdir, sagemaker_session):
@@ -528,6 +571,10 @@ def test_train_local_code(get_data_source_instance, tmpdir, sagemaker_session):
528571
@patch("sagemaker.local.local_session.LocalSession", Mock())
529572
@patch("sagemaker.local.image._stream_output", Mock())
530573
@patch("sagemaker.local.image._SageMakerContainer._cleanup", Mock())
574+
@patch(
575+
"sagemaker.local.image._SageMakerContainer._get_compose_cmd_prefix",
576+
Mock(return_value=["docker-compose"]),
577+
)
531578
@patch("sagemaker.local.data.get_data_source_instance")
532579
@patch("subprocess.Popen", Mock())
533580
def test_train_local_intermediate_output(get_data_source_instance, tmpdir, sagemaker_session):

0 commit comments

Comments
 (0)