Repository: simphony/simphony-remote

Issue:
FileConfig with tls=True or tls_verify=False only, leads to docker TLSParameterError 
On Linux, if my file config for the `remoteappmanager_config.py` ONLY contains the following:
```
$ cat remoteappmanager_config.py
tls = True
```

or
```
$ cat remoteappmanager_config.py
tls_verify = True
```

Then starting remoteappmanager gives this error
```
$ remoteappmanager --user=kit --port=45707 --cookie-name=jupyter-hub-token-kit --base-urlpath=/user/kit --hub-host= --hub-prefix=/hub/ --hub-api-url=http://172.16.253.129:8081/hub/api --ip=127.0.0.1 --proxy-api-url=http://127.0.0.1:8001/api/routes/ --config-file=remoteappmanager_config.py
...
[E 160720 14:33:43 web:1548] Uncaught exception GET /user/kit/ (127.0.0.1)
    HTTPServerRequest(protocol='http', host='127.0.0.1:8000', method='GET', uri='/user/kit/', version='HTTP/1.1', remote_ip='127.0.0.1', headers={'X-Forwarded-Proto': 'https', 'Accept-Language': 'en-US,en;q=0.5', 'X-Forwarded-For': '127.0.0.1', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'If-None-Match': '"dfba4089bef9b2e40bbd2a75ba5413bb8edbdd83"', 'Host': '127.0.0.1:8000', 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:39.0) Gecko/20100101 Firefox/39.0', 'Referer': 'https://127.0.0.1:8000/hub/login', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'X-Forwarded-Port': '8000', 'Cookie': 'jupyter-hub-token-kit="2|1:0|10:1469021623|21:jupyter-hub-token-kit|44:M2UzMjRlOGNhNDBjNGQ5ZWEwYjg3Njk0N2U0ODY4MmE=|1ec021fdad25b335a6b11b22ff198ce0860a0742a4d95c46a33a6d659d5f4e2d"'})
    Traceback (most recent call last):
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/traitlets/traitlets.py", line 501, in get
        value = obj._trait_values[self.name]
    KeyError: 'container_manager'
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/web.py", line 1469, in _execute
        result = yield result
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/gen.py", line 1015, in run
        value = future.result()
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/concurrent.py", line 237, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 3, in raise_exc_info
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/gen.py", line 1021, in run
        yielded = self.gen.throw(*exc_info)
      File "/mnt/hgfs/VM_shared/SimPhoNy/simphony-remote/remoteappmanager/handlers/home_handler.py", line 21, in get
        images_info = yield self._get_images_info()
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/gen.py", line 1015, in run
        value = future.result()
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/concurrent.py", line 237, in result
        raise_exc_info(self._exc_info)
      File "<string>", line 3, in raise_exc_info
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/tornado/gen.py", line 285, in wrapper
        yielded = next(result)
      File "/mnt/hgfs/VM_shared/SimPhoNy/simphony-remote/remoteappmanager/handlers/home_handler.py", line 184, in _get_images_info
        container_manager = self.application.container_manager
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/traitlets/traitlets.py", line 529, in __get__
        return self.get(obj, cls)
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/traitlets/traitlets.py", line 508, in get
        value = self._validate(obj, dynamic_default())
      File "/mnt/hgfs/VM_shared/SimPhoNy/simphony-remote/remoteappmanager/application.py", line 81, in _container_manager_default
        docker_config=self.file_config.docker_config()
      File "/mnt/hgfs/VM_shared/SimPhoNy/simphony-remote/remoteappmanager/file_config.py", line 155, in docker_config
        assert_hostname=True,
      File "/home/kit/.virtualenv/simremote/lib/python3.4/site-packages/docker/tls.py", line 47, in __init__
        'Path to a certificate and key files must be provided'
    docker.errors.TLSParameterError: Path to a certificate and key files must be provided through the client_config param. TLS configurations should map the Docker CLI client configurations. See https://docs.docker.com/engine/articles/https/ for API details.
```


Comments:

Patch:
diff --git a/remoteappmanager/docker/container.py b/remoteappmanager/docker/container.py
index cb484b0..f92304c 100644
--- a/remoteappmanager/docker/container.py
+++ b/remoteappmanager/docker/container.py
@@ -56,7 +56,7 @@ class Container(HasTraits):
             ")>")
 
     @classmethod
-    def from_docker_containers_dict(cls, docker_dict):
+    def from_docker_dict(cls, docker_dict):
         """Returns a Container object with the info given by a
         docker Client.
 
@@ -74,24 +74,66 @@ class Container(HasTraits):
         >>> # containers is a list of dict
         >>> containers = docker.Client().containers()
 
-        >>> Container.from_docker_containers_dict(containers[0])
+        >>> Container.from_docker_dict(containers[0])
         """
-        ip = cls.ip.default_value
-        port = cls.port.default_value
-
-        if docker_dict.get('Ports'):
-            ip = docker_dict['Ports'][0].get('IP', ip)
-            port = docker_dict['Ports'][0].get('PublicPort', port)
-
-        labels = docker_dict.get("Labels", {})
-        mapping_id = labels.get(SIMPHONY_NS+"mapping_id", "")
-        url_id = labels.get(SIMPHONY_NS+"url_id", "")
-
-        return cls(docker_id=docker_dict.get('Id', ''),
-                   name=docker_dict.get('Names', ('',))[0],
-                   image_name=docker_dict.get('Image', ''),
-                   image_id=docker_dict.get('ImageID', ''),
-                   mapping_id=mapping_id,
-                   ip=ip,
-                   port=port,
-                   url_id=url_id)
+
+        is_inspect_container_output = ("Config" in docker_dict)
+
+        kwargs = dict(
+            docker_id=docker_dict.get('Id') or '',
+            ip=cls.ip.default_value,
+            port=cls.port.default_value,
+        )
+
+        if is_inspect_container_output:
+            # It's a client.inspect_container() output
+
+            network_settings = docker_dict.get("NetworkSettings") or {}
+            ports = network_settings.get("Ports") or {}
+            # unfortunately, in the case of a running container, we don't have
+            # a single list. Instead, we have a dict where the keys are
+            # the "port identifier" (e.g. 8888/tcp) and the value is a list
+            # of dictionaries.
+            # We assume that we only have one, as above
+            if len(ports) > 1:
+                raise ValueError("Container Ports had more than one element.")
+
+            if len(ports):
+                port_values = list(ports.values())[0]
+                if len(port_values) > 1:
+                    raise ValueError("Container Ports values had "
+                                     "more than one element.")
+
+                if len(port_values):
+                    kwargs["ip"] = port_values[0].get("HostIp") or kwargs["ip"]
+                    kwargs["port"] = int(port_values[0].get("HostPort") or
+                                         kwargs["port"])
+
+            config = docker_dict.get("Config", {})
+            labels = config.get("Labels")
+
+            kwargs["image_name"] = config.get("Image")
+            kwargs["image_id"] = docker_dict["Image"]
+            kwargs["name"] = docker_dict["Name"]
+        else:
+            # It's a client.containers() output, so we have different rules.
+            ports = docker_dict.get('Ports') or []
+            if len(ports) > 1:
+                raise ValueError("Container Ports had more than one element.")
+
+            if len(ports):
+                kwargs["ip"] = ports[0].get('IP') or kwargs["ip"]
+                kwargs["port"] = int(ports[0].get('PublicPort') or
+                                     kwargs["port"])
+
+            labels = docker_dict.get("Labels") or {}
+
+            kwargs["image_name"] = docker_dict.get('Image') or ''
+            kwargs["image_id"] = docker_dict.get("ImageID") or ''
+            names = docker_dict.get("Names") or ('', )
+            kwargs["name"] = names[0]
+
+        kwargs["mapping_id"] = labels.get(SIMPHONY_NS+"mapping_id") or ""
+        kwargs["url_id"] = labels.get(SIMPHONY_NS+"url_id") or ""
+
+        return cls(**kwargs)
diff --git a/remoteappmanager/docker/container_manager.py b/remoteappmanager/docker/container_manager.py
index 1db82e5..7b85d53 100644
--- a/remoteappmanager/docker/container_manager.py
+++ b/remoteappmanager/docker/container_manager.py
@@ -166,8 +166,8 @@ class ContainerManager(LoggingMixin):
         infos = yield self.docker_client.containers(filters=filters)
         for info in infos:
             try:
-                container = Container.from_docker_containers_dict(info)
-            except Exception:
+                container = Container.from_docker_dict(info)
+            except ValueError:
                 self.log.exception("Unable to parse container info.")
                 continue
 
diff --git a/remoteappmanager/file_config.py b/remoteappmanager/file_config.py
index 2f93478..8966ed8 100644
--- a/remoteappmanager/file_config.py
+++ b/remoteappmanager/file_config.py
@@ -3,7 +3,6 @@ import os
 import tornado.options
 from docker import tls
 from traitlets import HasTraits, Int, Unicode, Bool, Dict
-from traitlets.utils.sentinel import Sentinel
 
 from remoteappmanager import paths
 from remoteappmanager.traitlets import set_traits_from_dict
@@ -110,12 +109,7 @@ class FileConfig(HasTraits):
         for traitlet_name, traitlet in self.traits().items():
             # tornado.OptionParser defines an option with a Python type
             # and performs type validation.
-            # traitlet.default_value may be a Sentinel value (e.g. Tuple,
-            # Dict, Instance), in which case we use the repr
-            default_value = traitlet.default_value
-
-            if type(default_value) is Sentinel:
-                default_value = eval(traitlet.default_value_repr())
+            default_value = getattr(self, traitlet_name)
 
             file_line_parser.define(
                 traitlet_name,
diff --git a/remoteappmanager/restresources/container.py b/remoteappmanager/restresources/container.py
index c3cd5e8..64bf36a 100644
--- a/remoteappmanager/restresources/container.py
+++ b/remoteappmanager/restresources/container.py
@@ -118,7 +118,7 @@ class Container(Resource):
         if not container_dict:
             return None
 
-        return DockerContainer.from_docker_containers_dict(container_dict[0])
+        return DockerContainer.from_docker_dict(container_dict[0])
 
     @gen.coroutine
     def _start_container(self, user_name, app, policy, mapping_id):


Test patch:
diff --git a/tests/docker/test_container.py b/tests/docker/test_container.py
index f5223dc..249eac9 100644
--- a/tests/docker/test_container.py
+++ b/tests/docker/test_container.py
@@ -1,7 +1,8 @@
 from unittest import TestCase
 
 from remoteappmanager.docker.container import Container
-from tests.utils import assert_containers_equal
+from tests.utils import assert_containers_equal, \
+    mock_docker_client_with_running_containers
 
 
 class TestContainer(TestCase):
@@ -42,7 +43,7 @@ class TestContainer(TestCase):
             'Status': 'Up 56 minutes'}
 
         # Container with public port
-        actual = Container.from_docker_containers_dict(container_dict)
+        actual = Container.from_docker_dict(container_dict)
         expected = Container(
             docker_id='248e45e717cd740ae763a1c565',
             name='/remoteexec-user-empty-ubuntu_3Alatest',
@@ -75,7 +76,7 @@ class TestContainer(TestCase):
             'Status': 'Up 56 minutes'}
 
         # Container without public port
-        actual = Container.from_docker_containers_dict(container_dict)
+        actual = Container.from_docker_dict(container_dict)
         expected = Container(
             docker_id='812c765d0549be0ab831ae8348',
             name='/remoteexec-user-empty-ubuntu_3Alatest',
@@ -86,3 +87,57 @@ class TestContainer(TestCase):
             url_id="8e2fe66d5de74db9bbab50c0d2f92b33")
 
         assert_containers_equal(self, actual, expected)
+
+    def test_from_docker_dict_inspect_container(self):
+        client = mock_docker_client_with_running_containers()
+        actual = Container.from_docker_dict(
+            client.inspect_container("id"))
+
+        expected = Container(
+            docker_id='35d88fe321c3d575ec3be64f54b8967ef49c0dc92395bf4c1e511ed3e6ae0c79',  # noqa
+            name='/remoteexec-username-simphonyproject_2Fsimphonic-mayavi_5F1',
+            image_name='simphonyproject/simphonic-mayavi',
+            image_id='sha256:f43b749341ee37b56e7bd8d99f09629f311aaec35a8045a39185b5659edef169',  # noqa
+            ip='0.0.0.0',
+            port=32782,
+            url_id="55555555555555555555555555555555",
+            mapping_id="1c08c87878634e90af43d799e90f61d2")
+
+        assert_containers_equal(self, actual, expected)
+
+    def test_multiple_ports_data(self):
+        client = mock_docker_client_with_running_containers()
+        docker_dict = client.inspect_container("id")
+        docker_dict["NetworkSettings"]["Ports"] = {
+            '8888/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '32782'}],
+            '8889/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '32783'}]
+        }
+        with self.assertRaises(ValueError):
+            Container.from_docker_dict(docker_dict)
+
+        docker_dict["NetworkSettings"]["Ports"] = {
+            '8888/tcp': [
+                {'HostIp': '0.0.0.0', 'HostPort': '32782'},
+                {'HostIp': '0.0.0.0', 'HostPort': '32783'}
+            ]
+        }
+        with self.assertRaises(ValueError):
+            Container.from_docker_dict(docker_dict)
+
+        docker_dict = client.containers()[0]
+        docker_dict["Ports"] = [
+             {
+                'IP': '0.0.0.0',
+                'PublicIP': 34567,
+                'PrivatePort': 22,
+                'Type': 'tcp'
+             },
+             {
+                'IP': '0.0.0.0',
+                'PublicIP': 34562,
+                'PrivatePort': 21,
+                'Type': 'tcp'
+             }
+        ]
+        with self.assertRaises(ValueError):
+            Container.from_docker_dict(docker_dict)
diff --git a/tests/test_file_config.py b/tests/test_file_config.py
index 22ce879..484d1b9 100644
--- a/tests/test_file_config.py
+++ b/tests/test_file_config.py
@@ -151,3 +151,15 @@ class TestFileConfig(TempMixin, unittest.TestCase):
         config = FileConfig(tls=True)
         self.assertNotEqual(config.tls_key, '')
         self.assertNotEqual(config.tls_cert, '')
+
+    def test_file_parsing_not_overriding_bug_131(self):
+        docker_config = textwrap.dedent('''
+            tls = True
+            ''')
+        with open(self.config_file, 'w') as fhandle:
+            print(docker_config, file=fhandle)
+
+        config = FileConfig()
+        config.parse_config(self.config_file)
+        self.assertNotEqual(config.tls_key, '')
+        self.assertNotEqual(config.tls_cert, '')
diff --git a/tests/utils.py b/tests/utils.py
index 9279636..2556bcd 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -110,6 +110,30 @@ def mock_docker_client_with_running_containers():
          'State': 'running',
          'Status': 'Up About an hour'},
     ]
+    client.inspect_container.return_value = {
+        'Config': {
+                   'Image': 'simphonyproject/simphonic-mayavi',
+                   'Labels': {'eu.simphony-project.docker.description': 'Ubuntu '
+                                                                        'machine '
+                                                                        'with '
+                                                                        'simphony '
+                                                                        'framework '
+                                                                        'preinstalled',
+                              'eu.simphony-project.docker.mapping_id': '1c08c87878634e90af43d799e90f61d2',
+                              'eu.simphony-project.docker.ui_name': 'Simphony '
+                                                                    'Framework '
+                                                                    '(w/ mayavi)',
+                              'eu.simphony-project.docker.url_id': '55555555555555555555555555555555',
+                              'eu.simphony-project.docker.user': 'username'},
+                    },
+        'Id': '35d88fe321c3d575ec3be64f54b8967ef49c0dc92395bf4c1e511ed3e6ae0c79',
+        'Image': 'sha256:f43b749341ee37b56e7bd8d99f09629f311aaec35a8045a39185b5659edef169',
+        'Name': '/remoteexec-username-simphonyproject_2Fsimphonic-mayavi_5F1',
+        'NetworkSettings': {
+                            'Ports': {'8888/tcp': [{'HostIp': '0.0.0.0',
+                                                    'HostPort': '32782'}]},
+                           },
+    }
 
     return client