Quellcode durchsuchen

Add a new fig command for retrieving the locally bound port of a service.

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin vor 11 Jahren
Ursprung
Commit
c48ee5caef

+ 4 - 0
docs/cli.md

@@ -28,6 +28,10 @@ Force stop service containers.
 
 View output from services.
 
+## port
+
+Print the public port for a port binding
+
 ## ps
 
 List containers.

+ 2 - 0
fig/cli/errors.py

@@ -9,6 +9,8 @@ class UserError(Exception):
     def __unicode__(self):
         return self.msg
 
+    __str__ = __unicode__
+
 
 class DockerNotFoundMac(UserError):
     def __init__(self):

+ 21 - 0
fig/cli/main.py

@@ -84,6 +84,7 @@ class TopLevelCommand(Command):
       help      Get help on a command
       kill      Kill containers
       logs      View output from containers
+      port      Print the public port for a port binding
       ps        List containers
       rm        Remove stopped containers
       run       Run a one-off command
@@ -148,6 +149,26 @@ class TopLevelCommand(Command):
         print("Attaching to", list_containers(containers))
         LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
 
+    def port(self, project, options):
+        """
+        Print the public port for a port binding.
+
+        Usage: port [options] SERVICE PRIVATE_PORT
+
+        Options:
+            --protocol=proto  tcp or udp (defaults to tcp)
+            --index=index     index of the container if there are multiple
+                              instances of a service (defaults to 1)
+        """
+        service = project.get_service(options['SERVICE'])
+        try:
+            container = service.get_container(number=options.get('--index') or 1)
+        except ValueError as e:
+            raise UserError(str(e))
+        print(container.get_local_port(
+            options['PRIVATE_PORT'],
+            protocol=options.get('--protocol') or 'tcp') or '')
+
     def ps(self, project, options):
         """
         List containers.

+ 19 - 10
fig/container.py

@@ -1,6 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import absolute_import
 
+from fig.packages import six
+
 
 class Container(object):
     """
@@ -63,17 +65,20 @@ class Container(object):
             return None
 
     @property
-    def human_readable_ports(self):
+    def ports(self):
         self.inspect_if_not_inspected()
-        if not self.dictionary['NetworkSettings']['Ports']:
-            return ''
-        ports = []
-        for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
-            if public:
-                ports.append('%s->%s' % (public[0]['HostPort'], private))
-            else:
-                ports.append(private)
-        return ', '.join(ports)
+        return self.dictionary['NetworkSettings']['Ports'] or {}
+
+    @property
+    def human_readable_ports(self):
+        def format_port(private, public):
+            if not public:
+                return private
+            return '{HostIp}:{HostPort}->{private}'.format(
+                private=private, **public[0])
+
+        return ', '.join(format_port(*item)
+                         for item in sorted(six.iteritems(self.ports)))
 
     @property
     def human_readable_state(self):
@@ -105,6 +110,10 @@ class Container(object):
         self.inspect_if_not_inspected()
         return self.dictionary['State']['Running']
 
+    def get_local_port(self, port, protocol='tcp'):
+        port = self.ports.get("%s/%s" % (port, protocol))
+        return "{HostIp}:{HostPort}".format(**port[0]) if port else None
+
     def start(self, **options):
         return self.client.start(self.id, **options)
 

+ 14 - 1
fig/service.py

@@ -78,9 +78,22 @@ class Service(object):
         name = get_container_name(container)
         if not name or not is_valid_name(name, one_off):
             return False
-        project, name, number = parse_name(name)
+        project, name, _number = parse_name(name)
         return project == self.project and name == self.name
 
+    def get_container(self, number=1):
+        """Return a :class:`fig.container.Container` for this service. The
+        container must be active, and match `number`.
+        """
+        for container in self.client.containers():
+            if not self.has_container(container):
+                continue
+            _, _, container_number = parse_name(get_container_name(container))
+            if container_number == number:
+                return Container.from_ps(self.client, container)
+
+        raise ValueError("No container found for %s_%s" % (self.name, number))
+
     def start(self, **options):
         for c in self.containers(stopped=True):
             self.start_container_if_stopped(c, **options)

+ 7 - 0
tests/fixtures/ports-figfile/fig.yml

@@ -0,0 +1,7 @@
+
+simple:
+  image: busybox:latest
+  command: /bin/sleep 300
+  ports:
+    - '3000'
+    - '9999:3001'

+ 19 - 3
tests/integration/cli_test.py

@@ -1,9 +1,11 @@
 from __future__ import absolute_import
-from .testcases import DockerClientTestCase
+import sys
+
+from fig.packages.six import StringIO
 from mock import patch
+
+from .testcases import DockerClientTestCase
 from fig.cli.main import TopLevelCommand
-from fig.packages.six import StringIO
-import sys
 
 
 class CLITestCase(DockerClientTestCase):
@@ -213,3 +215,17 @@ class CLITestCase(DockerClientTestCase):
         self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
         self.assertEqual(len(project.get_service('simple').containers()), 0)
         self.assertEqual(len(project.get_service('another').containers()), 0)
+
+    def test_port(self):
+        self.command.base_dir = 'tests/fixtures/ports-figfile'
+        self.command.dispatch(['up', '-d'], None)
+        container = self.project.get_service('simple').get_container()
+
+        @patch('sys.stdout', new_callable=StringIO)
+        def get_port(number, mock_stdout):
+            self.command.dispatch(['port', 'simple', str(number)], None)
+            return mock_stdout.getvalue().rstrip()
+
+        self.assertEqual(get_port(3000), container.get_local_port(3000))
+        self.assertEqual(get_port(3001), "0.0.0.0:9999")
+        self.assertEqual(get_port(3002), "")

+ 54 - 34
tests/unit/container_test.py

@@ -8,18 +8,28 @@ from fig.container import Container
 
 
 class ContainerTest(unittest.TestCase):
+
+
+    def setUp(self):
+        self.container_dict = {
+            "Id": "abc",
+            "Image": "busybox:latest",
+            "Command": "sleep 300",
+            "Created": 1387384730,
+            "Status": "Up 8 seconds",
+            "Ports": None,
+            "SizeRw": 0,
+            "SizeRootFs": 0,
+            "Names": ["/figtest_db_1"],
+            "NetworkSettings": {
+                "Ports": {},
+            },
+        }
+
     def test_from_ps(self):
-        container = Container.from_ps(None, {
-            "Id":"abc",
-            "Image":"busybox:latest",
-            "Command":"sleep 300",
-            "Created":1387384730,
-            "Status":"Up 8 seconds",
-            "Ports":None,
-            "SizeRw":0,
-            "SizeRootFs":0,
-            "Names":["/figtest_db_1"]
-        }, has_been_inspected=True)
+        container = Container.from_ps(None,
+                                      self.container_dict,
+                                      has_been_inspected=True)
         self.assertEqual(container.dictionary, {
             "Id": "abc",
             "Image":"busybox:latest",
@@ -42,35 +52,21 @@ class ContainerTest(unittest.TestCase):
         })
 
     def test_number(self):
-        container = Container.from_ps(None, {
-            "Id":"abc",
-            "Image":"busybox:latest",
-            "Command":"sleep 300",
-            "Created":1387384730,
-            "Status":"Up 8 seconds",
-            "Ports":None,
-            "SizeRw":0,
-            "SizeRootFs":0,
-            "Names":["/figtest_db_1"]
-        }, has_been_inspected=True)
+        container = Container.from_ps(None,
+                                      self.container_dict,
+                                      has_been_inspected=True)
         self.assertEqual(container.number, 1)
 
     def test_name(self):
-        container = Container.from_ps(None, {
-            "Id":"abc",
-            "Image":"busybox:latest",
-            "Command":"sleep 300",
-            "Names":["/figtest_db_1"]
-        }, has_been_inspected=True)
+        container = Container.from_ps(None,
+                                      self.container_dict,
+                                      has_been_inspected=True)
         self.assertEqual(container.name, "figtest_db_1")
 
     def test_name_without_project(self):
-        container = Container.from_ps(None, {
-            "Id":"abc",
-            "Image":"busybox:latest",
-            "Command":"sleep 300",
-            "Names":["/figtest_db_1"]
-        }, has_been_inspected=True)
+        container = Container.from_ps(None,
+                                      self.container_dict,
+                                      has_been_inspected=True)
         self.assertEqual(container.name_without_project, "db_1")
 
     def test_inspect_if_not_inspected(self):
@@ -85,3 +81,27 @@ class ContainerTest(unittest.TestCase):
 
         container.inspect_if_not_inspected()
         self.assertEqual(mock_client.inspect_container.call_count, 1)
+
+    def test_human_readable_ports_none(self):
+        container = Container(None, self.container_dict, has_been_inspected=True)
+        self.assertEqual(container.human_readable_ports, '')
+
+    def test_human_readable_ports_public_and_private(self):
+        self.container_dict['NetworkSettings']['Ports'].update({
+            "45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
+            "45453/tcp": [],
+        })
+        container = Container(None, self.container_dict, has_been_inspected=True)
+
+        expected = "45453/tcp, 0.0.0.0:49197->45454/tcp"
+        self.assertEqual(container.human_readable_ports, expected)
+
+    def test_get_local_port(self):
+        self.container_dict['NetworkSettings']['Ports'].update({
+            "45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
+        })
+        container = Container(None, self.container_dict, has_been_inspected=True)
+
+        self.assertEqual(
+            container.get_local_port(45454, protocol='tcp'),
+            '0.0.0.0:49197')

+ 23 - 2
tests/unit/service_test.py

@@ -5,6 +5,8 @@ import os
 from .. import unittest
 import mock
 
+from fig.packages import docker
+
 from fig import Service
 from fig.service import (
     ConfigError,
@@ -97,14 +99,33 @@ class ServiceTest(unittest.TestCase):
 
     def test_split_domainname_weird(self):
         service = Service('foo',
-                hostname = 'name.sub',
-                domainname = 'domain.tld',
+                hostname='name.sub',
+                domainname='domain.tld',
             )
         service.next_container_name = lambda x: 'foo'
         opts = service._get_container_create_options({})
         self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
         self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
 
+    def test_get_container_not_found(self):
+        mock_client = mock.create_autospec(docker.Client)
+        mock_client.containers.return_value = []
+        service = Service('foo', client=mock_client)
+
+        self.assertRaises(ValueError, service.get_container)
+
+    @mock.patch('fig.service.Container', autospec=True)
+    def test_get_container(self, mock_container_class):
+        mock_client = mock.create_autospec(docker.Client)
+        container_dict = dict(Name='default_foo_2')
+        mock_client.containers.return_value = [container_dict]
+        service = Service('foo', client=mock_client)
+
+        container = service.get_container(number=2)
+        self.assertEqual(container, mock_container_class.from_ps.return_value)
+        mock_container_class.from_ps.assert_called_once_with(
+            mock_client, container_dict)
+
 
 class ServiceVolumesTest(unittest.TestCase):