diff --git a/compose/cli/main.py b/compose/cli/main.py index ea3199899..810b13f54 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -24,6 +24,7 @@ from ..config import ConfigurationError from ..config import parse_environment from ..config.environment import Environment from ..config.serialize import serialize_config +from ..config.types import VolumeSpec from ..const import IS_WINDOWS_PLATFORM from ..errors import StreamParseError from ..progress_stream import StreamOutputError @@ -729,7 +730,7 @@ class TopLevelCommand(object): running. If you do not want to start linked services, use `docker-compose run --no-deps SERVICE COMMAND [ARGS...]`. - Usage: run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] + Usage: run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] Options: -d Detached mode: Run container in the background, print @@ -743,6 +744,7 @@ class TopLevelCommand(object): -p, --publish=[] Publish a container's port(s) to the host --service-ports Run command with the service's ports enabled and mapped to the host. + -v, --volume=[] Bind mount a volume (default []) -T Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY. -w, --workdir="" Working directory inside the container @@ -1086,6 +1088,10 @@ def build_container_options(options, detach, command): if options['--workdir']: container_options['working_dir'] = options['--workdir'] + if options['--volume']: + volumes = [VolumeSpec.parse(i) for i in options['--volume']] + container_options['volumes'] = volumes + return container_options diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index f4ad717b0..31853a11b 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import datetime import json import os +import os.path import signal import subprocess import time @@ -18,6 +19,7 @@ import yaml from docker import errors from .. import mock +from ..helpers import create_host_file from compose.cli.command import get_project from compose.container import Container from compose.project import OneOffFilter @@ -557,6 +559,45 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(old_ids, new_ids) + def test_run_one_off_with_volume(self): + self.base_dir = 'tests/fixtures/simple-composefile-volume-ready' + volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files')) + create_host_file(self.client, os.path.join(volume_path, 'example.txt')) + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + 'simple', + 'test', '-f', '/data/example.txt' + ], returncode=0) + # FIXME: does not work with Python 3 + # assert cmd_result.stdout.strip() == 'FILE_CONTENT' + + def test_run_one_off_with_multiple_volumes(self): + self.base_dir = 'tests/fixtures/simple-composefile-volume-ready' + volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files')) + create_host_file(self.client, os.path.join(volume_path, 'example.txt')) + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + '-v', '{}:/data1'.format(volume_path), + 'simple', + 'test', '-f', '/data/example.txt' + ], returncode=0) + # FIXME: does not work with Python 3 + # assert cmd_result.stdout.strip() == 'FILE_CONTENT' + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + '-v', '{}:/data1'.format(volume_path), + 'simple', + 'test', '-f' '/data1/example.txt' + ], returncode=0) + # FIXME: does not work with Python 3 + # assert cmd_result.stdout.strip() == 'FILE_CONTENT' + def test_create_with_force_recreate_and_no_recreate(self): self.dispatch( ['create', '--force-recreate', '--no-recreate'], diff --git a/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml new file mode 100644 index 000000000..98a7d23b7 --- /dev/null +++ b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml @@ -0,0 +1,2 @@ +simple: + image: busybox:latest diff --git a/tests/fixtures/simple-composefile-volume-ready/files/example.txt b/tests/fixtures/simple-composefile-volume-ready/files/example.txt new file mode 100644 index 000000000..edb4d3390 --- /dev/null +++ b/tests/fixtures/simple-composefile-volume-ready/files/example.txt @@ -0,0 +1 @@ +FILE_CONTENT diff --git a/tests/helpers.py b/tests/helpers.py index 4b422a6a0..59efd2557 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,6 +1,8 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os + from compose.config.config import ConfigDetails from compose.config.config import ConfigFile from compose.config.config import load @@ -15,3 +17,30 @@ def build_config_details(contents, working_dir='working_dir', filename='filename working_dir, [ConfigFile(filename, contents)], ) + + +def create_host_file(client, filename): + dirname = os.path.dirname(filename) + + with open(filename, 'r') as fh: + content = fh.read() + + container = client.create_container( + 'busybox:latest', + ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], + volumes={dirname: {}}, + host_config=client.create_host_config( + binds={dirname: {'bind': dirname, 'ro': False}}, + network_mode='none', + ), + ) + try: + client.start(container) + exitcode = client.wait(container) + + if exitcode != 0: + output = client.logs(container) + raise Exception( + "Container exited with code {}:\n{}".format(exitcode, output)) + finally: + client.remove_container(container, force=True) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 28762cd20..f0d21456b 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -10,6 +10,7 @@ from docker.errors import NotFound from .. import mock from ..helpers import build_config as load_config +from ..helpers import create_host_file from .testcases import DockerClientTestCase from compose.config import config from compose.config import ConfigurationError @@ -1517,30 +1518,3 @@ class ProjectTest(DockerClientTestCase): assert 'svc1' in svc2.get_dependency_names() with pytest.raises(NoHealthCheckConfigured): svc1.is_healthy() - - -def create_host_file(client, filename): - dirname = os.path.dirname(filename) - - with open(filename, 'r') as fh: - content = fh.read() - - container = client.create_container( - 'busybox:latest', - ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], - volumes={dirname: {}}, - host_config=client.create_host_config( - binds={dirname: {'bind': dirname, 'ro': False}}, - network_mode='none', - ), - ) - try: - client.start(container) - exitcode = client.wait(container) - - if exitcode != 0: - output = client.logs(container) - raise Exception( - "Container exited with code {}:\n{}".format(exitcode, output)) - finally: - client.remove_container(container, force=True) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index fb0511f0a..f9ce240a3 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -119,6 +119,7 @@ class CLITestCase(unittest.TestCase): '--entrypoint': None, '--service-ports': None, '--publish': [], + '--volume': [], '--rm': None, '--name': None, '--workdir': None, @@ -153,6 +154,7 @@ class CLITestCase(unittest.TestCase): '--entrypoint': None, '--service-ports': None, '--publish': [], + '--volume': [], '--rm': None, '--name': None, '--workdir': None, @@ -175,6 +177,7 @@ class CLITestCase(unittest.TestCase): '--entrypoint': None, '--service-ports': None, '--publish': [], + '--volume': [], '--rm': True, '--name': None, '--workdir': None,