Cleanup, externalized into modules, fixed auth

This commit is contained in:
shin- 2013-09-10 20:31:03 +02:00
parent dc3937faa8
commit 99c9eadc82
6 changed files with 212 additions and 132 deletions

View File

@ -1 +1,2 @@
from .client import Client
import auth

108
docker/auth.py Normal file
View File

@ -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

View File

@ -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 <user>/<repo>""")
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 = {

45
docker/unixconn.py Normal file
View File

@ -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)

34
docker/utils.py Normal file
View File

@ -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)

View File

@ -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()