Browse Source

Merge pull request #1824 from mrfuxi/run-with-ports

Allow manual port mapping when using "run" command
Daniel Nephin 10 years ago
parent
commit
b87c09b9fc

+ 11 - 1
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,

+ 1 - 1
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

+ 1 - 0
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' \

+ 5 - 0
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

+ 38 - 0
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()

+ 31 - 0
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,
+            })