Merge pull request #436 from docker/0.7.0-release

0.7.0 release
This commit is contained in:
Joffrey F 2014-12-19 09:38:43 -08:00
commit 9b166eea75
9 changed files with 132 additions and 107 deletions

View File

@ -115,8 +115,8 @@ class Client(requests.Session):
command = shlex.split(str(command))
if isinstance(environment, dict):
environment = [
(six.text_type('{0}={1}').format(k, v)
for k, v in environment.items())
six.text_type('{0}={1}').format(k, v)
for k, v in six.iteritems(environment)
]
if isinstance(mem_limit, six.string_types):
@ -911,63 +911,7 @@ class Client(requests.Session):
restart_policy=None, cap_add=None, cap_drop=None, devices=None,
extra_hosts=None):
start_config = {}
if isinstance(container, dict):
container = container.get('Id')
if isinstance(lxc_conf, dict):
formatted = []
for k, v in six.iteritems(lxc_conf):
formatted.append({'Key': k, 'Value': str(v)})
lxc_conf = formatted
if lxc_conf:
start_config['LxcConf'] = lxc_conf
if binds:
start_config['Binds'] = utils.convert_volume_binds(binds)
if port_bindings:
start_config['PortBindings'] = utils.convert_port_bindings(
port_bindings
)
if publish_all_ports:
start_config['PublishAllPorts'] = publish_all_ports
if links:
if isinstance(links, dict):
links = six.iteritems(links)
formatted_links = [
'{0}:{1}'.format(k, v) for k, v in sorted(links)
]
start_config['Links'] = formatted_links
if extra_hosts:
if isinstance(extra_hosts, dict):
extra_hosts = six.iteritems(extra_hosts)
formatted_extra_hosts = [
'{0}:{1}'.format(k, v) for k, v in sorted(extra_hosts)
]
start_config['ExtraHosts'] = formatted_extra_hosts
if privileged:
start_config['Privileged'] = privileged
if utils.compare_version('1.10', self._version) >= 0:
if dns is not None:
start_config['Dns'] = dns
if volumes_from is not None:
if isinstance(volumes_from, six.string_types):
volumes_from = volumes_from.split(',')
start_config['VolumesFrom'] = volumes_from
else:
if utils.compare_version('1.10', self._version) < 0:
if dns is not None:
raise errors.APIError(
'dns is only supported for API version >= 1.10'
@ -976,23 +920,18 @@ class Client(requests.Session):
raise errors.APIError(
'volumes_from is only supported for API version >= 1.10'
)
if dns_search:
start_config['DnsSearch'] = dns_search
if network_mode:
start_config['NetworkMode'] = network_mode
start_config = utils.create_host_config(
binds=binds, port_bindings=port_bindings, lxc_conf=lxc_conf,
publish_all_ports=publish_all_ports, links=links, dns=dns,
privileged=privileged, dns_search=dns_search, cap_add=cap_add,
cap_drop=cap_drop, volumes_from=volumes_from, devices=devices,
network_mode=network_mode, restart_policy=restart_policy,
extra_hosts=extra_hosts
)
if restart_policy:
start_config['RestartPolicy'] = restart_policy
if cap_add:
start_config['CapAdd'] = cap_add
if cap_drop:
start_config['CapDrop'] = cap_drop
if devices:
start_config['Devices'] = utils.parse_devices(devices)
if isinstance(container, dict):
container = container.get('Id')
url = self._url("/containers/{0}/start".format(container))
if not start_config:

View File

@ -300,12 +300,16 @@ def create_host_config(
binds=None, port_bindings=None, lxc_conf=None,
publish_all_ports=False, links=None, privileged=False,
dns=None, dns_search=None, volumes_from=None, network_mode=None,
restart_policy=None, cap_add=None, cap_drop=None, devices=None
restart_policy=None, cap_add=None, cap_drop=None, devices=None,
extra_hosts=None
):
host_config = {
'Privileged': privileged,
'PublishAllPorts': publish_all_ports,
}
host_config = {}
if privileged:
host_config['Privileged'] = privileged
if publish_all_ports:
host_config['PublishAllPorts'] = publish_all_ports
if dns_search:
host_config['DnsSearch'] = dns_search
@ -341,7 +345,14 @@ def create_host_config(
port_bindings
)
host_config['PublishAllPorts'] = publish_all_ports
if extra_hosts:
if isinstance(extra_hosts, dict):
extra_hosts = [
'{0}:{1}'.format(k, v)
for k, v in sorted(six.iteritems(extra_hosts))
]
host_config['ExtraHosts'] = extra_hosts
if links:
if isinstance(links, dict):
@ -358,6 +369,8 @@ def create_host_config(
for k, v in six.iteritems(lxc_conf):
formatted.append({'Key': k, 'Value': str(v)})
lxc_conf = formatted
host_config['LxcConf'] = lxc_conf
if lxc_conf:
host_config['LxcConf'] = lxc_conf
return host_config

View File

@ -1 +1 @@
version = "0.6.1-dev"
version = "0.7.0"

View File

@ -54,7 +54,7 @@ correct value (e.g `gzip`).
* nocache (bool): Don't use the cache when set to `True`
* rm (bool): Remove intermediate containers
* stream (bool): Return a blocking generator you can iterate over to retrieve
build output as it happens
build output as it happens
* timeout (int): HTTP timeout
* custom_context (bool): Optional if using `fileobj`
* encoding (str): The encoding for a stream. Set to `gzip` for compressing
@ -385,6 +385,8 @@ Nearly identical to the `docker login` command, but non-interactive.
* email (str): The email for the registry account
* registry (str): URL to the registry. Ex:`https://index.docker.io/v1/`
* reauth (bool): Whether refresh existing authentication on the docker server.
* dockercfg_path (str): Use a custom path for the .dockercfg file
(default `$HOME/.dockercfg`)
**Returns** (dict): The response from the login request
@ -636,6 +638,7 @@ from. Optionally a single string joining container id's with commas
`['on-failure', 'always']`
* cap_add (list of str): See note above
* cap_drop (list of str): See note above
* extra_hosts (dict): custom host-to-IP mappings (host:ip)
```python
>>> from docker import Client

View File

@ -1,6 +1,43 @@
Change Log
==========
0.7.0
-----
### Breaking changes
* Passing `dns` or `volumes_from` in `Client.start` with API version < 1.10
will now raise an exception (previously only triggered a warning)
### Features
* Added support for `host_config` in `Client.create_container`
* Added utility method `docker.utils.create_host_config` to help build a
proper `HostConfig` dictionary.
* Added support for the `pull` parameter in `Client.build`
* Added support for the `forcerm` parameter in `Client.build`
* Added support for `extra_hosts` in `Client.start`
* Added support for a custom `timeout` in `Client.wait`
* Added support for custom `.dockercfg` loading in `Client.login`
(`dockercfg_path` argument)
### Bugfixes
* Fixed a bug where some output wouldn't be streamed properly in streaming
chunked responses
* Fixed a bug where the `devices` param didn't recognize the proper delimiter
* `Client.login` now properly expands the `registry` URL if provided.
* Fixed a bug where unicode characters in passed for `environment` in
`create_container` would break.
### Miscellaneous
* Several unit tests and integration tests improvements.
* `Client` constructor now enforces passing the `version` parameter as a
string.
* Build context files are now ordered by filename when creating the archive
(for consistency with docker mainline behavior)
0.6.0
-----
* **This version introduces breaking changes!**

View File

@ -81,6 +81,7 @@ for example:
* restart_policy (dict): "Name" param must be one of `['on-failure', 'always']`
* cap_add (list of str): Add kernel capabilities
* cap_drop (list of str): Drop kernel capabilities
* extra_hosts (dict): custom host-to-IP mappings (host:ip)
**Returns** (dict) HostConfig dictionary

View File

@ -35,6 +35,7 @@ DEFAULT_BASE_URL = os.environ.get('DOCKER_HOST')
warnings.simplefilter('error')
create_host_config = docker.utils.create_host_config
compare_version = docker.utils.compare_version
class BaseTestCase(unittest.TestCase):
@ -43,6 +44,9 @@ class BaseTestCase(unittest.TestCase):
tmp_folders = []
def setUp(self):
if six.PY2:
self.assertRegex = self.assertRegexpMatches
self.assertCountEqual = self.assertItemsEqual
self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=5)
self.tmp_imgs = []
self.tmp_containers = []
@ -62,6 +66,7 @@ class BaseTestCase(unittest.TestCase):
pass
for folder in self.tmp_folders:
shutil.rmtree(folder)
self.client.close()
#########################
# INFORMATION TESTS #
@ -134,7 +139,7 @@ class TestListContainers(BaseTestCase):
self.assertIn('Command', retrieved)
self.assertEqual(retrieved['Command'], six.text_type('true'))
self.assertIn('Image', retrieved)
self.assertRegexpMatches(retrieved['Image'], r'busybox:.*')
self.assertRegex(retrieved['Image'], r'busybox:.*')
self.assertIn('Status', retrieved)
#####################
@ -178,6 +183,8 @@ class TestCreateContainerWithBinds(BaseTestCase):
logs = self.client.logs(container_id)
os.unlink(shared_file)
if six.PY3:
logs = logs.decode('utf-8')
self.assertIn(filename, logs)
@ -208,6 +215,8 @@ class TestStartContainerWithBinds(BaseTestCase):
logs = self.client.logs(container_id)
os.unlink(shared_file)
if six.PY3:
logs = logs.decode('utf-8')
self.assertIn(filename, logs)
@ -517,12 +526,12 @@ class TestPort(BaseTestCase):
def runTest(self):
port_bindings = {
1111: ('127.0.0.1', '4567'),
2222: ('127.0.0.1', '4568')
'1111': ('127.0.0.1', '4567'),
'2222': ('127.0.0.1', '4568')
}
container = self.client.create_container(
'busybox', ['sleep', '60'], ports=port_bindings.keys(),
'busybox', ['sleep', '60'], ports=list(port_bindings.keys()),
host_config=create_host_config(port_bindings=port_bindings)
)
id = container['Id']
@ -546,12 +555,12 @@ class TestStartWithPortBindings(BaseTestCase):
def runTest(self):
port_bindings = {
1111: ('127.0.0.1', '4567'),
2222: ('127.0.0.1', '4568')
'1111': ('127.0.0.1', '4567'),
'2222': ('127.0.0.1', '4568')
}
container = self.client.create_container(
'busybox', ['sleep', '60'], ports=port_bindings.keys()
'busybox', ['sleep', '60'], ports=list(port_bindings.keys())
)
id = container['Id']
@ -668,7 +677,7 @@ class TestCreateContainerWithVolumesFrom(BaseTestCase):
self.client.start(container3_id)
info = self.client.inspect_container(res2['Id'])
self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names)
self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names)
class TestCreateContainerWithLinks(BaseTestCase):
@ -713,6 +722,8 @@ class TestCreateContainerWithLinks(BaseTestCase):
self.assertEqual(self.client.wait(container3_id), 0)
logs = self.client.logs(container3_id)
if six.PY3:
logs = logs.decode('utf-8')
self.assertIn('{0}_NAME='.format(link_env_prefix1), logs)
self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs)
self.assertIn('{0}_NAME='.format(link_env_prefix2), logs)
@ -749,7 +760,7 @@ class TestStartContainerWithVolumesFrom(BaseTestCase):
self.client.start(container3_id, volumes_from=vol_names)
info = self.client.inspect_container(res2['Id'])
self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names)
self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names)
class TestStartContainerWithLinks(BaseTestCase):
@ -793,6 +804,8 @@ class TestStartContainerWithLinks(BaseTestCase):
self.assertEqual(self.client.wait(container3_id), 0)
logs = self.client.logs(container3_id)
if six.PY3:
logs = logs.decode('utf-8')
self.assertIn('{0}_NAME='.format(link_env_prefix1), logs)
self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs)
self.assertIn('{0}_NAME='.format(link_env_prefix2), logs)
@ -939,6 +952,7 @@ class TestRemoveLink(BaseTestCase):
class TestPull(BaseTestCase):
def runTest(self):
self.client.close()
self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10)
try:
self.client.remove_image('busybox')
@ -947,7 +961,7 @@ class TestPull(BaseTestCase):
res = self.client.pull('busybox')
self.assertEqual(type(res), six.text_type)
self.assertGreaterEqual(
self.client.images('busybox'), 1
len(self.client.images('busybox')), 1
)
img_info = self.client.inspect_image('busybox')
self.assertIn('Id', img_info)
@ -955,6 +969,7 @@ class TestPull(BaseTestCase):
class TestPullStream(BaseTestCase):
def runTest(self):
self.client.close()
self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10)
try:
self.client.remove_image('busybox')
@ -962,9 +977,11 @@ class TestPullStream(BaseTestCase):
pass
stream = self.client.pull('busybox', stream=True)
for chunk in stream:
if six.PY3:
chunk = chunk.decode('utf-8')
json.loads(chunk) # ensure chunk is a single, valid JSON blob
self.assertGreaterEqual(
self.client.images('busybox'), 1
len(self.client.images('busybox')), 1
)
img_info = self.client.inspect_image('busybox')
self.assertIn('Id', img_info)
@ -1013,7 +1030,7 @@ class TestRemoveImage(BaseTestCase):
class TestBuild(BaseTestCase):
def runTest(self):
if self.client._version >= 1.8:
if compare_version(self.client._version, '1.8') < 0:
return
script = io.BytesIO('\n'.join([
'FROM busybox',
@ -1055,6 +1072,8 @@ class TestBuildStream(BaseTestCase):
stream = self.client.build(fileobj=script, stream=True)
logs = ''
for chunk in stream:
if six.PY3:
chunk = chunk.decode('utf-8')
json.loads(chunk) # ensure chunk is a single, valid JSON blob
logs += chunk
self.assertNotEqual(logs, '')
@ -1075,13 +1094,15 @@ class TestBuildFromStringIO(BaseTestCase):
stream = self.client.build(fileobj=script, stream=True)
logs = ''
for chunk in stream:
if six.PY3:
chunk = chunk.decode('utf-8')
logs += chunk
self.assertNotEqual(logs, '')
class TestBuildWithAuth(BaseTestCase):
def runTest(self):
if self.client._version < 1.9:
if compare_version(self.client._version, '1.9') >= 0:
return
k = 'K4104GON3P4Q6ZUJFZRRC2ZQTBJ5YT0UMZD7TGT7ZVIR8Y05FAH2TJQI6Y90SMIB'
@ -1097,6 +1118,8 @@ class TestBuildWithAuth(BaseTestCase):
stream = self.client.build(fileobj=script, stream=True)
logs = ''
for chunk in stream:
if six.PY3:
chunk = chunk.decode('utf-8')
logs += chunk
self.assertNotEqual(logs, '')
@ -1105,7 +1128,7 @@ class TestBuildWithAuth(BaseTestCase):
class TestBuildWithDockerignore(Cleanup, BaseTestCase):
def runTest(self):
if self.client._version < 1.8:
if compare_version(self.client._version, '1.8') >= 0:
return
base_dir = tempfile.mkdtemp()
@ -1136,6 +1159,8 @@ class TestBuildWithDockerignore(Cleanup, BaseTestCase):
stream = self.client.build(path=base_dir, stream=True)
logs = ''
for chunk in stream:
if six.PY3:
chunk = chunk.decode('utf-8')
logs += chunk
self.assertFalse('node_modules' in logs)
self.assertTrue('not-ignored' in logs)
@ -1171,16 +1196,17 @@ class TestLoadConfig(BaseTestCase):
def runTest(self):
folder = tempfile.mkdtemp()
self.tmp_folders.append(folder)
f = open(os.path.join(folder, '.dockercfg'), 'w')
cfg_path = os.path.join(folder, '.dockercfg')
f = open(cfg_path, 'w')
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
f.write('auth = {0}\n'.format(auth_))
f.write('email = sakuya@scarlet.net')
f.close()
cfg = docker.auth.load_config(folder)
cfg = docker.auth.load_config(cfg_path)
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['username'], 'sakuya')
self.assertEqual(cfg['password'], 'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)
@ -1189,17 +1215,18 @@ class TestLoadJSONConfig(BaseTestCase):
def runTest(self):
folder = tempfile.mkdtemp()
self.tmp_folders.append(folder)
cfg_path = os.path.join(folder, '.dockercfg')
f = open(os.path.join(folder, '.dockercfg'), 'w')
auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii')
email_ = 'sakuya@scarlet.net'
f.write('{{"{}": {{"auth": "{}", "email": "{}"}}}}\n'.format(
f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format(
docker.auth.INDEX_URL, auth_, email_))
f.close()
cfg = docker.auth.load_config(folder)
cfg = docker.auth.load_config(cfg_path)
self.assertNotEqual(cfg[docker.auth.INDEX_URL], None)
cfg = cfg[docker.auth.INDEX_URL]
self.assertEqual(cfg['username'], b'sakuya')
self.assertEqual(cfg['password'], b'izayoi')
self.assertEqual(cfg['username'], 'sakuya')
self.assertEqual(cfg['password'], 'izayoi')
self.assertEqual(cfg['email'], 'sakuya@scarlet.net')
self.assertEqual(cfg.get('Auth'), None)
@ -1248,4 +1275,5 @@ class UnixconnTestCase(unittest.TestCase):
if __name__ == '__main__':
c = docker.Client(base_url=DEFAULT_BASE_URL)
c.pull('busybox')
c.close()
unittest.main()

View File

@ -733,7 +733,6 @@ class DockerClientTest(Cleanup, unittest.TestCase):
args = fake_request.call_args
self.assertEqual(args[0][0], url_prefix + 'containers/create')
data = json.loads(args[1]['data'])
self.assertEqual(data['HostConfig']['PublishAllPorts'], False)
port_bindings = data['HostConfig']['PortBindings']
self.assertTrue('1111/tcp' in port_bindings)
self.assertTrue('2222/tcp' in port_bindings)

View File

@ -5,7 +5,8 @@ import unittest
from docker.client import Client
from docker.errors import DockerException
from docker.utils import (
parse_repository_tag, parse_host, convert_filters, kwargs_from_env
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
create_host_config
)
@ -95,6 +96,10 @@ class UtilsTest(unittest.TestCase):
for filters, expected in tests:
self.assertEqual(convert_filters(filters), expected)
def test_create_host_config(self):
empty_config = create_host_config()
self.assertEqual(empty_config, {})
if __name__ == '__main__':
unittest.main()