Repository: docker/docker-py

Issue:
Using a docker/config.json file causes "TypeError: string indices must be integers"
Using a ~/.docker/config.json file causes docker-compose to output a Python error. @dnephin in https://github.com/docker/compose/issues/2697#issuecomment-172936366 suggests that this is an issue to be raised with the docker-py project instead of the compose project.

So here's a simple config.json file, and a dummy docker-compose.yml file which demonstrate the issue:

```
$ cat ~/.docker/config.json 
{
  "detachKeys": "ctrl-q,ctrl-u,ctrl-i,ctrl-t"
}
$ cat docker-compose.yml 
version: '2'
services:
  s1:
    image: ubuntu
$ docker-compose ps
Traceback (most recent call last):
  File "<string>", line 3, in <module>
  File "/compose/compose/cli/main.py", line 55, in main
  File "/compose/compose/cli/docopt_command.py", line 23, in sys_dispatch
  File "/compose/compose/cli/docopt_command.py", line 26, in dispatch
  File "/compose/compose/cli/main.py", line 172, in perform_command
  File "/compose/compose/cli/command.py", line 52, in project_from_options
  File "/compose/compose/cli/command.py", line 85, in get_project
  File "/compose/compose/cli/command.py", line 66, in get_client
  File "/compose/compose/cli/docker_client.py", line 37, in docker_client
  File "/compose/venv/lib/python2.7/site-packages/docker/client.py", line 56, in __init__
  File "/compose/venv/lib/python2.7/site-packages/docker/auth/auth.py", line 182, in load_config
  File "/compose/venv/lib/python2.7/site-packages/docker/auth/auth.py", line 118, in parse_auth
TypeError: string indices must be integers
docker-compose returned -1
```

And if we delete the config and try again:

```
$ rm ~/.docker/config.json 
$ docker-compose ps
Name   Command   State   Ports 
------------------------------
```

I'm using Docker Toolbox 1.10.0 on OSX 10.11.3. The docker container is being run on a VirtualBox VM managed by docker-machine. Here are some more version numbers:

```
$ docker version
Client:
 Version:      1.10.0
 API version:  1.22
 Go version:   go1.5.3
 Git commit:   590d5108
 Built:        Thu Feb  4 18:18:11 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.10.0
 API version:  1.22
 Go version:   go1.5.3
 Git commit:   590d5108
 Built:        Thu Feb  4 19:55:25 2016
 OS/Arch:      linux/amd64
$ docker-compose version
docker-compose version 1.6.0, build d99cad6
docker-py version: 1.7.0
CPython version: 2.7.9
OpenSSL version: OpenSSL 1.0.1j 15 Oct 2014
```

Discussion:
dnephin: LGTM

Patch:
diff --git a/docker/auth/auth.py b/docker/auth/auth.py
index 399dae2b..eedb7944 100644
--- a/docker/auth/auth.py
+++ b/docker/auth/auth.py
@@ -46,7 +46,7 @@ def resolve_repository_name(repo_name):
 
 def resolve_index_name(index_name):
     index_name = convert_to_hostname(index_name)
-    if index_name == 'index.'+INDEX_NAME:
+    if index_name == 'index.' + INDEX_NAME:
         index_name = INDEX_NAME
     return index_name
 
@@ -102,12 +102,14 @@ def encode_header(auth):
     return base64.urlsafe_b64encode(auth_json)
 
 
-def parse_auth(entries):
+def parse_auth(entries, raise_on_error=False):
     """
     Parses authentication entries
 
     Args:
-      entries: Dict of authentication entries.
+      entries:        Dict of authentication entries.
+      raise_on_error: If set to true, an invalid format will raise
+                      InvalidConfigFile
 
     Returns:
       Authentication registry.
@@ -115,6 +117,19 @@ def parse_auth(entries):
 
     conf = {}
     for registry, entry in six.iteritems(entries):
+        if not (isinstance(entry, dict) and 'auth' in entry):
+            log.debug(
+                'Config entry for key {0} is not auth config'.format(registry)
+            )
+            # We sometimes fall back to parsing the whole config as if it was
+            # the auth config by itself, for legacy purposes. In that case, we
+            # fail silently and return an empty conf if any of the keys is not
+            # formatted properly.
+            if raise_on_error:
+                raise errors.InvalidConfigFile(
+                    'Invalid configuration for registry {0}'.format(registry)
+                )
+            return {}
         username, password = decode_auth(entry['auth'])
         log.debug(
             'Found entry (registry={0}, username={1})'
@@ -170,7 +185,7 @@ def load_config(config_path=None):
             res = {}
             if data.get('auths'):
                 log.debug("Found 'auths' section")
-                res.update(parse_auth(data['auths']))
+                res.update(parse_auth(data['auths'], raise_on_error=True))
             if data.get('HttpHeaders'):
                 log.debug("Found 'HttpHeaders' section")
                 res.update({'HttpHeaders': data['HttpHeaders']})


Test patch:
diff --git a/tests/unit/auth_test.py b/tests/unit/auth_test.py
index 3fba602c..921aae00 100644
--- a/tests/unit/auth_test.py
+++ b/tests/unit/auth_test.py
@@ -433,3 +433,32 @@ class LoadConfigTest(base.Cleanup, base.BaseTestCase):
 
             self.assertEqual(cfg['Name'], 'Spike')
             self.assertEqual(cfg['Surname'], 'Spiegel')
+
+    def test_load_config_unknown_keys(self):
+        folder = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, folder)
+        dockercfg_path = os.path.join(folder, 'config.json')
+        config = {
+            'detachKeys': 'ctrl-q, ctrl-u, ctrl-i'
+        }
+        with open(dockercfg_path, 'w') as f:
+            json.dump(config, f)
+
+        cfg = auth.load_config(dockercfg_path)
+        assert cfg == {}
+
+    def test_load_config_invalid_auth_dict(self):
+        folder = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, folder)
+        dockercfg_path = os.path.join(folder, 'config.json')
+        config = {
+            'auths': {
+                'scarlet.net': {'sakuya': 'izayoi'}
+            }
+        }
+        with open(dockercfg_path, 'w') as f:
+            json.dump(config, f)
+
+        self.assertRaises(
+            errors.InvalidConfigFile, auth.load_config, dockercfg_path
+        )