diff --git a/docker/__init__.py b/docker/__init__.py index 3ff722bf..3f7c1ce1 100644 --- a/docker/__init__.py +++ b/docker/__init__.py @@ -1 +1,2 @@ from .client import Client +import auth \ No newline at end of file diff --git a/docker/auth.py b/docker/auth.py new file mode 100644 index 00000000..f62b6014 --- /dev/null +++ b/docker/auth.py @@ -0,0 +1,108 @@ +import base64 +import json +import os + +import six + + +def swap_protocol(url): + if url.startswith('http://'): + return url.replace('http://', 'https://', 1) + if url.startswith('https://'): + return url.replace('https://', 'http://', 1) + return url + + +def expand_registry_url(hostname): + if hostname.startswith('http:') or hostname.startswith('https:'): + if '/' not in hostname[9:]: + hostname = hostname + '/v1/' + return hostname + #FIXME: ping https then fallback to http + return 'http://' + hostname + '/v1/' + + +def resolve_repository_name(repo_name): + if '://' in repo_name: + raise ValueError('Repository name can not contain a' + 'scheme ({0})'.format(repo_name)) + parts = repo_name.split('/', 1) + if not '.' in parts[0] and not ':' in parts[0] and parts[0] != 'localhost': + # This is a docker index repo (ex: foo/bar or ubuntu) + return 'https://index.docker.io/v1/', repo_name + if len(parts) < 2: + raise ValueError('Invalid repository name ({0})'.format(repo_name)) + + if 'index.docker.io' in parts[0]: + raise ValueError('Invalid repository name,' + 'try "{0}" instead'.format(parts[1])) + + return expand_registry_url(parts[0]), parts[1] + + +def resolve_authconfig(authconfig, registry): + if registry == 'https://index.docker.io/v1/' or registry == '': + # default to the index server + return authconfig['Configs']['https://index.docker.io/v1/'] + # if its not the index server there are three cases: + # + # 1. this is a full config url -> it should be used as is + # 2. it could be a full url, but with the wrong protocol + # 3. it can be the hostname optionally with a port + # + # as there is only one auth entry which is fully qualified we need to start + # parsing and matching + if '/' not in registry: + registry = registry + '/v1/' + if not registry.startswith('http:') and not registry.startswith('https:'): + registry = 'https://' + registry + + if registry in authconfig['Configs']: + return authconfig['Configs'][registry] + elif swap_protocol(registry) in authconfig['Configs']: + return authconfig['Configs'][swap_protocol(registry)] + return {} + + +def decode_auth(auth): + s = base64.b64decode(auth) + login, pwd = s.split(':') + return login, pwd + + +def encode_header(auth): + auth_json = json.dumps(auth) + return base64.b64encode(auth_json) + + +def load_config(root=None): + if root is None: + root = os.environ['HOME'] + config_file = { + 'Configs': {}, + 'rootPath': root + } + f = open(os.path.join(root, '.dockercfg')) + try: + config_file['Configs'] = json.load(f) + for k, conf in six.iteritems(config_file['Configs']): + conf['Username'], conf['Password'] = decode_auth(conf['auth']) + del conf['auth'] + config_file['Configs'][k] = conf + except Exception: + f.seek(0) + buf = [] + for line in f: + k, v = line.split(' = ') + buf.append(v) + if len(buf) < 2: + raise Exception("The Auth config file is empty") + user, pwd = decode_auth(buf[0]) + config_file['Configs']['https://index.docker.io/v1/'] = { + 'Username': user, + 'Password': pwd, + 'Email': buf[1] + } + finally: + f.close() + return config_file diff --git a/docker/client.py b/docker/client.py index 47707a97..69ab31ed 100644 --- a/docker/client.py +++ b/docker/client.py @@ -1,74 +1,26 @@ -import base64 import json import logging -import os import re -import six import shlex -import tarfile -import tempfile -import six -import httplib -import socket import requests -from requests.exceptions import HTTPError -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.connectionpool import HTTPConnectionPool +import requests.exceptions +import six -if six.PY3: - from io import StringIO -else: - from StringIO import StringIO - -class UnixHTTPConnection(httplib.HTTPConnection, object): - def __init__(self, base_url, unix_socket): - httplib.HTTPConnection.__init__(self, 'localhost') - self.base_url = base_url - self.unix_socket = unix_socket - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.base_url.replace("unix:/","")) - self.sock = sock - - def _extract_path(self, url): - #remove the base_url entirely.. - return url.replace(self.base_url, "") - - def request(self, method, url, **kwargs): - url = self._extract_path(self.unix_socket) - super(UnixHTTPConnection, self).request(method, url, **kwargs) - - -class UnixHTTPConnectionPool(HTTPConnectionPool): - def __init__(self, base_url, socket_path): - self.socket_path = socket_path - self.base_url = base_url - super(UnixHTTPConnectionPool, self).__init__(self, 'localhost') - - def _new_conn(self): - return UnixHTTPConnection(self.base_url, self.socket_path) - - -class UnixAdapter(HTTPAdapter): - def __init__(self, base_url): - self.base_url = base_url - super(UnixAdapter, self).__init__() - - def get_connection(self, socket_path, proxies=None): - return UnixHTTPConnectionPool(self.base_url, socket_path) +import auth +import unixconn +import utils class Client(requests.Session): def __init__(self, base_url="unix://var/run/docker.sock", version="1.4"): super(Client, self).__init__() - self.mount('unix://', UnixAdapter(base_url)) + self.mount('unix://', unixconn.UnixAdapter(base_url)) self.base_url = base_url self._version = version try: - self._cfg = self._load_config() - except: + self._cfg = auth.load_config() + except Exception: pass def _url(self, path): @@ -89,7 +41,7 @@ class Client(requests.Session): http_error_msg += ' "%s"' % response.content if http_error_msg: - raise HTTPError(http_error_msg, response=response) + raise requests.exceptions.HTTPError(http_error_msg, response=response) def _result(self, response, json=False): if response.status_code != 200 and response.status_code != 201: @@ -123,27 +75,6 @@ class Client(requests.Session): 'VolumesFrom': volumes_from, } - def _mkbuildcontext(self, dockerfile): - f = tempfile.TemporaryFile() - t = tarfile.open(mode='w', fileobj=f) - if isinstance(dockerfile, StringIO): - dfinfo = tarfile.TarInfo('Dockerfile') - dfinfo.size = dockerfile.len - else: - dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile') - t.addfile(dfinfo, dockerfile) - t.close() - f.seek(0) - return f - - def _tar(self, path): - f = tempfile.TemporaryFile() - t = tarfile.open(mode='w', fileobj=f) - t.add(path, arcname='.') - t.close() - f.seek(0) - return f - def _post_json(self, url, data, **kwargs): # Go <1.1 can't unserialize null to a string # so we do this disgusting thing here. @@ -158,43 +89,6 @@ class Client(requests.Session): kwargs['headers']['Content-Type'] = 'application/json' return self.post(url, json.dumps(data2), **kwargs) - def _decode_auth(self, auth): - s = base64.b64decode(auth) - login, pwd = s.split(':') - return login, pwd - - def _load_config(self, root=None): - if root is None: - root = os.environ['HOME'] - config_file = { - 'Configs': {}, - 'rootPath': root - } - f = open(os.path.join(root, '.dockercfg')) - try: - config_file['Configs'] = json.load(f) - for k, conf in six.iteritems(config_file['Configs']): - conf['Username'], conf['Password'] = self._decode_auth(conf['auth']) - del conf['auth'] - config_file['Configs'][k] = conf - except: - f.seek(0) - buf = [] - for line in f: - k, v = line.split(' = ') - buf.append(v) - if len(buf) < 2: - raise Exception("The Auth config file is empty") - user, pwd = self._decode_auth(buf[0]) - config_file['Configs']['https://index.docker.io/v1/'] = { - 'Username': user, - 'Password': pwd, - 'Email': buf[1] - } - finally: - f.close() - return config_file - def attach(self, container): params = { 'stdout': 1, @@ -221,12 +115,12 @@ class Client(requests.Session): raise Exception("Either path or fileobj needs to be provided.") if fileobj is not None: - context = self._mkbuildcontext(fileobj) + context = utils.mkbuildcontext(fileobj) elif (path.startswith('http://') or path.startswith('https://') or path.startswith('git://') or path.startswith('github.com/')): remote = path else: - context = self._tar(path) + context = utils.tar(path) u = self._url('/build') params = { 't': tag, 'remote': remote, 'q': quiet, 'nocache': nocache } @@ -361,7 +255,7 @@ class Client(requests.Session): } res = self._result(self._post_json(url, req_data), True) try: - self._cfg = self._load_config() + self._cfg = auth.load_config() finally: return res @@ -399,14 +293,15 @@ class Client(requests.Session): return self._result(self.post(u, None, params=params)) def push(self, repository): - if repository.count("/") < 1: - raise ValueError("""Impossible to push a \"root\" repository. - Please rename your repository in /""") + registry, repository = auth.resolve_repository_name(repository) if getattr(self, '_cfg', None) is None: - self._cfg = self._load_config() + self._cfg = auth.load_config() + authcfg = auth.resolve_authconfig(self._cfg, registry) u = self._url("/images/{0}/push".format(repository)) - return self._result( - self._post_json(u, self._cfg['Configs']['https://index.docker.io/v1/'])) + if utils.compare_version('1.5', self._version) >= 0: + headers = { 'X-Registry-Auth': auth.encode_header(authcfg) } + return self._result(self._post_json(u, None, headers=headers)) + return self._result(self._post_json(u, authcfg)) def remove_container(self, *args, **kwargs): params = { diff --git a/docker/unixconn.py b/docker/unixconn.py new file mode 100644 index 00000000..48c8da59 --- /dev/null +++ b/docker/unixconn.py @@ -0,0 +1,45 @@ +import httplib +import requests.adapters +import requests.packages.urllib3.connectionpool +import socket + +HTTPConnectionPool = requests.packages.urllib3.connectionpool.HTTPConnectionPool + + +class UnixHTTPConnection(httplib.HTTPConnection, object): + def __init__(self, base_url, unix_socket): + httplib.HTTPConnection.__init__(self, 'localhost') + self.base_url = base_url + self.unix_socket = unix_socket + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.base_url.replace("unix:/","")) + self.sock = sock + + def _extract_path(self, url): + #remove the base_url entirely.. + return url.replace(self.base_url, "") + + def request(self, method, url, **kwargs): + url = self._extract_path(self.unix_socket) + super(UnixHTTPConnection, self).request(method, url, **kwargs) + + +class UnixHTTPConnectionPool(HTTPConnectionPool): + def __init__(self, base_url, socket_path): + self.socket_path = socket_path + self.base_url = base_url + super(UnixHTTPConnectionPool, self).__init__(self, 'localhost') + + def _new_conn(self): + return UnixHTTPConnection(self.base_url, self.socket_path) + + +class UnixAdapter(requests.adapters.HTTPAdapter): + def __init__(self, base_url): + self.base_url = base_url + super(UnixAdapter, self).__init__() + + def get_connection(self, socket_path, proxies=None): + return UnixHTTPConnectionPool(self.base_url, socket_path) diff --git a/docker/utils.py b/docker/utils.py new file mode 100644 index 00000000..1c477155 --- /dev/null +++ b/docker/utils.py @@ -0,0 +1,34 @@ +import six +import tarfile +import tempfile + +if six.PY3: + from io import StringIO +else: + from StringIO import StringIO + + +def mkbuildcontext(dockerfile): + f = tempfile.TemporaryFile() + t = tarfile.open(mode='w', fileobj=f) + if isinstance(dockerfile, StringIO): + dfinfo = tarfile.TarInfo('Dockerfile') + dfinfo.size = dockerfile.len + else: + dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile') + t.addfile(dfinfo, dockerfile) + t.close() + f.seek(0) + return f + + +def tar(self, path): + f = tempfile.TemporaryFile() + t = tarfile.open(mode='w', fileobj=f) + t.add(path, arcname='.') + t.close() + f.seek(0) + return f + +def compare_version(v1, v2): + return float(v2) - float(v1) \ No newline at end of file diff --git a/tests/test.py b/tests/test.py index 3cd6f36f..3649570e 100644 --- a/tests/test.py +++ b/tests/test.py @@ -5,7 +5,6 @@ import tempfile import time import unittest - import docker import six @@ -365,19 +364,17 @@ class TestLoadConfig(BaseTestCase): def runTest(self): folder = tempfile.mkdtemp() f = open(os.path.join(folder, '.dockercfg'), 'w') - auth = base64.b64encode('sakuya:izayoi') - f.write('auth = {0}\n'.format(auth)) + auth_ = base64.b64encode('sakuya:izayoi') + f.write('auth = {0}\n'.format(auth_)) f.write('email = sakuya@scarlet.net') f.close() - cfg = self.client._load_config(folder) - self.assertNotEqual(cfg['Configs']['index.docker.io'], None) - cfg = cfg['Configs']['index.docker.io'] + cfg = docker.auth.load_config(folder) + self.assertNotEqual(cfg['Configs']['https://index.docker.io/v1/'], None) + cfg = cfg['Configs']['https://index.docker.io/v1/'] self.assertEqual(cfg['Username'], 'sakuya') self.assertEqual(cfg['Password'], 'izayoi') self.assertEqual(cfg['Email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('Auth'), None) - - if __name__ == '__main__': unittest.main()