diff --git a/compose/cli/main.py b/compose/cli/main.py index 56f6c0505..6c2a8edb6 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -282,7 +282,7 @@ class TopLevelCommand(Command): running. If you do not want to start linked services, use `docker-compose run --no-deps SERVICE COMMAND [ARGS...]`. - Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] + Usage: run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] Options: --allow-insecure-ssl Deprecated - no effect. @@ -293,6 +293,7 @@ class TopLevelCommand(Command): -u, --user="" Run as specified username or uid --no-deps Don't start linked services. --rm Remove container after run. Ignored in detached mode. + -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. -T Disable pseudo-tty allocation. By default `docker-compose run` @@ -344,6 +345,15 @@ class TopLevelCommand(Command): if not options['--service-ports']: container_options['ports'] = [] + if options['--publish']: + container_options['ports'] = options.get('--publish') + + if options['--publish'] and options['--service-ports']: + raise UserError( + 'Service port mapping and manual port mapping ' + 'can not be used togather' + ) + try: container = service.create_container( quiet=True, diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index e7d8cb3f8..128428d9a 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -248,7 +248,7 @@ _docker-compose_run() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --no-deps --rm --service-ports -T --user -u" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --no-deps --rm --service-ports --publish -p -T --user -u" -- "$cur" ) ) ;; *) __docker-compose_services_all diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 9af21a98b..9ac7e7560 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -221,6 +221,7 @@ __docker-compose_subcommand () { '(-u --user)'{-u,--user=-}'[Run as specified username or uid]:username or uid:_users' \ "--no-deps[Don't start linked services.]" \ '--rm[Remove container after run. Ignored in detached mode.]' \ + "--publish[Run command with manually mapped container's port(s) to the host.]" \ "--service-ports[Run command with the service's ports enabled and mapped to the host.]" \ '-T[Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.]' \ '(-):services:__docker-compose_services' \ diff --git a/docs/reference/run.md b/docs/reference/run.md index 5ea9a61be..93ae0212b 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -22,6 +22,7 @@ Options: -u, --user="" Run as specified username or uid --no-deps Don't start linked services. --rm Remove container after run. Ignored in detached mode. +-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. -T Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY. ``` @@ -38,6 +39,10 @@ The second difference is the `docker-compose run` command does not create any of $ docker-compose run --service-ports web python manage.py shell +Alternatively manual port mapping can be specified. Same as when running Docker's `run` command - using `--publish` or `-p` options: + + $ docker-compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell + If you start a service configured with links, the `run` command first checks to see if the linked service is running and starts the service if it is stopped. Once all the linked services are running, the `run` executes the command you passed it. So, for example, you could run: $ docker-compose run db psql -h db -U docker diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index e844fa2a3..ef789e19c 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -346,6 +346,44 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(port_range[0], "0.0.0.0:49153") self.assertEqual(port_range[1], "0.0.0.0:49154") + @patch('dockerpty.start') + def test_run_service_with_explicitly_maped_ports(self, __): + + # create one off container + self.command.base_dir = 'tests/fixtures/ports-composefile' + self.command.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple'], None) + container = self.project.get_service('simple').containers(one_off=True)[0] + + # get port information + port_short = container.get_local_port(3000) + port_full = container.get_local_port(3001) + + # close all one off containers we just created + container.stop() + + # check the ports + self.assertEqual(port_short, "0.0.0.0:30000") + self.assertEqual(port_full, "0.0.0.0:30001") + + @patch('dockerpty.start') + def test_run_service_with_explicitly_maped_ip_ports(self, __): + + # create one off container + self.command.base_dir = 'tests/fixtures/ports-composefile' + self.command.dispatch(['run', '-d', '-p', '127.0.0.1:30000:3000', '--publish', '127.0.0.1:30001:3001', 'simple'], None) + container = self.project.get_service('simple').containers(one_off=True)[0] + + # get port information + port_short = container.get_local_port(3000) + port_full = container.get_local_port(3001) + + # close all one off containers we just created + container.stop() + + # check the ports + self.assertEqual(port_short, "127.0.0.1:30000") + self.assertEqual(port_full, "127.0.0.1:30001") + def test_rm(self): service = self.project.get_service('simple') service.create_container() diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 3f5000329..e11f6f14a 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -7,6 +7,7 @@ import docker import mock from compose.cli.docopt_command import NoSuchCommand +from compose.cli.errors import UserError from compose.cli.main import TopLevelCommand from compose.service import Service @@ -108,6 +109,7 @@ class CLITestCase(unittest.TestCase): '-T': None, '--entrypoint': None, '--service-ports': None, + '--publish': [], '--rm': None, }) @@ -136,6 +138,7 @@ class CLITestCase(unittest.TestCase): '-T': None, '--entrypoint': None, '--service-ports': None, + '--publish': [], '--rm': None, }) _, _, call_kwargs = mock_client.create_container.mock_calls[0] @@ -160,7 +163,35 @@ class CLITestCase(unittest.TestCase): '-T': None, '--entrypoint': None, '--service-ports': None, + '--publish': [], '--rm': True, }) _, _, call_kwargs = mock_client.create_container.mock_calls[0] self.assertFalse('RestartPolicy' in call_kwargs['host_config']) + + def test_command_manula_and_service_ports_together(self): + command = TopLevelCommand() + mock_client = mock.create_autospec(docker.Client) + mock_project = mock.Mock(client=mock_client) + mock_project.get_service.return_value = Service( + 'service', + client=mock_client, + restart='always', + image='someimage', + ) + + with self.assertRaises(UserError): + command.run(mock_project, { + 'SERVICE': 'service', + 'COMMAND': None, + '-e': [], + '--user': None, + '--no-deps': None, + '--allow-insecure-ssl': None, + '-d': True, + '-T': None, + '--entrypoint': None, + '--service-ports': True, + '--publish': ['80:80'], + '--rm': None, + })