Merge pull request #2062 from docker/3.4.0-release

3.4.0 Release
This commit is contained in:
Joffrey F 2018-06-18 15:22:42 -07:00 committed by GitHub
commit f70545e89a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 191 additions and 53 deletions

View File

@ -302,7 +302,8 @@ class BuildApiMixin(object):
# credentials/native_store.go#L68-L83
for registry in self._auth_configs.get('auths', {}).keys():
auth_data[registry] = auth.resolve_authconfig(
self._auth_configs, registry
self._auth_configs, registry,
credstore_env=self.credstore_env,
)
else:
auth_data = self._auth_configs.get('auths', {}).copy()
@ -341,4 +342,9 @@ def process_dockerfile(dockerfile, path):
)
# Dockerfile is inside the context - return path relative to context root
return (os.path.relpath(abs_dockerfile, path), None)
if dockerfile == abs_dockerfile:
# Only calculate relpath if necessary to avoid errors
# on Windows client -> Linux Docker
# see https://github.com/docker/compose/issues/5969
dockerfile = os.path.relpath(abs_dockerfile, path)
return (dockerfile, None)

View File

@ -83,6 +83,8 @@ class APIClient(
:py:class:`~docker.tls.TLSConfig` object to use custom
configuration.
user_agent (str): Set a custom user agent for requests to the server.
credstore_env (dict): Override environment variables when calling the
credential store process.
"""
__attrs__ = requests.Session.__attrs__ + ['_auth_configs',
@ -93,7 +95,8 @@ class APIClient(
def __init__(self, base_url=None, version=None,
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False,
user_agent=DEFAULT_USER_AGENT, num_pools=DEFAULT_NUM_POOLS):
user_agent=DEFAULT_USER_AGENT, num_pools=DEFAULT_NUM_POOLS,
credstore_env=None):
super(APIClient, self).__init__()
if tls and not base_url:
@ -109,6 +112,7 @@ class APIClient(
self._auth_configs = auth.load_config(
config_dict=self._general_configs
)
self.credstore_env = credstore_env
base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)

View File

@ -128,7 +128,9 @@ class DaemonApiMixin(object):
elif not self._auth_configs:
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
authcfg = auth.resolve_authconfig(
self._auth_configs, registry, credstore_env=self.credstore_env,
)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \

View File

@ -44,7 +44,10 @@ class PluginApiMixin(object):
"""
url = self._url('/plugins/create')
with utils.create_archive(root=plugin_data_dir, gzip=gzip) as archv:
with utils.create_archive(
root=plugin_data_dir, gzip=gzip,
files=set(utils.build.walk(plugin_data_dir, []))
) as archv:
res = self._post(url, params={'name': name}, data=archv)
self._raise_for_status(res)
return True
@ -167,8 +170,16 @@ class PluginApiMixin(object):
'remote': name,
}
headers = {}
registry, repo_name = auth.resolve_repository_name(name)
header = auth.get_config_header(self, registry)
if header:
headers['X-Registry-Auth'] = header
url = self._url('/plugins/privileges')
return self._result(self._get(url, params=params), True)
return self._result(
self._get(url, params=params, headers=headers), True
)
@utils.minimum_version('1.25')
@utils.check_resource('name')

View File

@ -44,7 +44,9 @@ def get_config_header(client, registry):
"No auth config in memory - loading from filesystem"
)
client._auth_configs = load_config()
authcfg = resolve_authconfig(client._auth_configs, registry)
authcfg = resolve_authconfig(
client._auth_configs, registry, credstore_env=client.credstore_env
)
# Do not fail here if no authentication exists for this
# specific registry as we can have a readonly pull. Just
# put the header if we can.
@ -76,7 +78,7 @@ def get_credential_store(authconfig, registry):
)
def resolve_authconfig(authconfig, registry=None):
def resolve_authconfig(authconfig, registry=None, credstore_env=None):
"""
Returns the authentication data from the given auth configuration for a
specific registry. As with the Docker client, legacy entries in the config
@ -91,7 +93,7 @@ def resolve_authconfig(authconfig, registry=None):
'Using credentials store "{0}"'.format(store_name)
)
cfg = _resolve_authconfig_credstore(
authconfig, registry, store_name
authconfig, registry, store_name, env=credstore_env
)
if cfg is not None:
return cfg
@ -115,13 +117,14 @@ def resolve_authconfig(authconfig, registry=None):
return None
def _resolve_authconfig_credstore(authconfig, registry, credstore_name):
def _resolve_authconfig_credstore(authconfig, registry, credstore_name,
env=None):
if not registry or registry == INDEX_NAME:
# The ecosystem is a little schizophrenic with index.docker.io VS
# docker.io - in that case, it seems the full URL is necessary.
registry = INDEX_URL
log.debug("Looking for auth entry for {0}".format(repr(registry)))
store = dockerpycreds.Store(credstore_name)
store = dockerpycreds.Store(credstore_name, environment=env)
try:
data = store.get(registry)
res = {

View File

@ -33,6 +33,8 @@ class DockerClient(object):
:py:class:`~docker.tls.TLSConfig` object to use custom
configuration.
user_agent (str): Set a custom user agent for requests to the server.
credstore_env (dict): Override environment variables when calling the
credential store process.
"""
def __init__(self, *args, **kwargs):
self.api = APIClient(*args, **kwargs)
@ -66,6 +68,8 @@ class DockerClient(object):
assert_hostname (bool): Verify the hostname of the server.
environment (dict): The environment to read environment variables
from. Default: the value of ``os.environ``
credstore_env (dict): Override environment variables when calling
the credential store process.
Example:
@ -77,8 +81,9 @@ class DockerClient(object):
"""
timeout = kwargs.pop('timeout', DEFAULT_TIMEOUT_SECONDS)
version = kwargs.pop('version', None)
return cls(timeout=timeout, version=version,
**kwargs_from_env(**kwargs))
return cls(
timeout=timeout, version=version, **kwargs_from_env(**kwargs)
)
# Resources
@property

View File

@ -211,5 +211,5 @@ class NetworkCollection(Collection):
return networks
def prune(self, filters=None):
self.client.api.prune_networks(filters=filters)
return self.client.api.prune_networks(filters=filters)
prune.__doc__ = APIClient.prune_networks.__doc__

View File

@ -126,7 +126,7 @@ class Service(Model):
service_mode = ServiceMode('replicated', replicas)
return self.client.api.update_service(self.id, self.version,
service_mode,
mode=service_mode,
fetch_current_spec=True)
def force_update(self):
@ -276,7 +276,7 @@ CONTAINER_SPEC_KWARGS = [
'labels',
'mounts',
'open_stdin',
'privileges'
'privileges',
'read_only',
'secrets',
'stop_grace_period',

View File

@ -1,14 +1,10 @@
import six
import requests.adapters
import socket
from six.moves import http_client as httplib
from .. import constants
if six.PY3:
import http.client as httplib
else:
import httplib
try:
import requests.packages.urllib3 as urllib3
except ImportError:

View File

@ -57,6 +57,8 @@ class CancellableStream(object):
else:
sock = sock_fp._sock
if isinstance(sock, urllib3.contrib.pyopenssl.WrappedSocket):
sock = sock.socket
sock.shutdown(socket.SHUT_RDWR)
sock.close()

View File

@ -82,7 +82,7 @@ class ContainerSpec(dict):
args (:py:class:`list`): Arguments to the command.
hostname (string): The hostname to set on the container.
env (dict): Environment variables.
dir (string): The working directory for commands to run in.
workdir (string): The working directory for commands to run in.
user (string): The user inside the container.
labels (dict): A map of labels to associate with the service.
mounts (:py:class:`list`): A list of specifications for mounts to be

View File

@ -1,6 +1,7 @@
import errno
import os
import select
import socket as pysocket
import struct
import six
@ -28,6 +29,8 @@ def read(socket, n=4096):
try:
if hasattr(socket, 'recv'):
return socket.recv(n)
if six.PY3 and isinstance(socket, getattr(pysocket, 'SocketIO')):
return socket.read(n)
return os.read(socket.fileno(), n)
except EnvironmentError as e:
if e.errno not in recoverable_errors:

View File

@ -1,2 +1,2 @@
version = "3.3.0"
version = "3.4.0"
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])

View File

@ -1,6 +1,31 @@
Change log
==========
3.4.0
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/51?closed=1)
### Features
* The `APIClient` and `DockerClient` constructors now accept a `credstore_env`
parameter. When set, values in this dictionary are added to the environment
when executing the credential store process.
### Bugfixes
* `DockerClient.networks.prune` now properly returns the operation's result
* Fixed a bug that caused custom Dockerfile paths in a subfolder of the build
context to be invalidated, preventing these builds from working
* The `plugin_privileges` method can now be called for plugins requiring
authentication to access
* Fixed a bug that caused attempts to read a data stream over an unsecured TCP
socket to crash on Windows clients
* Fixed a bug where using the `read_only` parameter when creating a service using
the `DockerClient` was being ignored
* Fixed an issue where `Service.scale` would not properly update the service's
mode, causing the operation to fail silently
3.3.0
-----

View File

@ -3,7 +3,7 @@ asn1crypto==0.22.0
backports.ssl-match-hostname==3.5.0.1
cffi==1.10.0
cryptography==1.9
docker-pycreds==0.2.3
docker-pycreds==0.3.0
enum34==1.1.6
idna==2.5
ipaddress==1.0.18

View File

@ -13,7 +13,7 @@ requirements = [
'requests >= 2.14.2, != 2.18.0',
'six >= 1.4.0',
'websocket-client >= 0.32.0',
'docker-pycreds >= 0.2.3'
'docker-pycreds >= 0.3.0'
]
extras_require = {

View File

@ -415,18 +415,20 @@ class BuildTest(BaseAPIIntegrationTest):
f.write('hello world')
with open(os.path.join(base_dir, '.dockerignore'), 'w') as f:
f.write('.dockerignore\n')
df = tempfile.NamedTemporaryFile()
self.addCleanup(df.close)
df.write(('\n'.join([
'FROM busybox',
'COPY . /src',
'WORKDIR /src',
])).encode('utf-8'))
df.flush()
df_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, df_dir)
df_name = os.path.join(df_dir, 'Dockerfile')
with open(df_name, 'wb') as df:
df.write(('\n'.join([
'FROM busybox',
'COPY . /src',
'WORKDIR /src',
])).encode('utf-8'))
df.flush()
img_name = random_name()
self.tmp_imgs.append(img_name)
stream = self.client.build(
path=base_dir, dockerfile=df.name, tag=img_name,
path=base_dir, dockerfile=df_name, tag=img_name,
decode=True
)
lines = []
@ -472,6 +474,39 @@ class BuildTest(BaseAPIIntegrationTest):
[b'.', b'..', b'file.txt', b'custom.dockerfile']
) == sorted(lsdata)
def test_build_in_context_nested_dockerfile(self):
base_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, base_dir)
with open(os.path.join(base_dir, 'file.txt'), 'w') as f:
f.write('hello world')
subdir = os.path.join(base_dir, 'hello', 'world')
os.makedirs(subdir)
with open(os.path.join(subdir, 'custom.dockerfile'), 'w') as df:
df.write('\n'.join([
'FROM busybox',
'COPY . /src',
'WORKDIR /src',
]))
img_name = random_name()
self.tmp_imgs.append(img_name)
stream = self.client.build(
path=base_dir, dockerfile='hello/world/custom.dockerfile',
tag=img_name, decode=True
)
lines = []
for chunk in stream:
lines.append(chunk)
assert 'Successfully tagged' in lines[-1]['stream']
ctnr = self.client.create_container(img_name, 'ls -a')
self.tmp_containers.append(ctnr)
self.client.start(ctnr)
lsdata = self.client.logs(ctnr).strip().split(b'\n')
assert len(lsdata) == 4
assert sorted(
[b'.', b'..', b'file.txt', b'hello']
) == sorted(lsdata)
def test_build_in_context_abs_dockerfile(self):
base_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, base_dir)

View File

@ -491,6 +491,9 @@ class CreateContainerTest(BaseAPIIntegrationTest):
assert rule in self.client.logs(ctnr).decode('utf-8')
@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
class VolumeBindTest(BaseAPIIntegrationTest):
def setUp(self):
super(VolumeBindTest, self).setUp()
@ -507,9 +510,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
['touch', os.path.join(self.mount_dest, self.filename)],
)
@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
def test_create_with_binds_rw(self):
container = self.run_with_volume(
@ -525,9 +525,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, True)
@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
def test_create_with_binds_ro(self):
self.run_with_volume(
False,
@ -548,9 +545,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, False)
@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
@requires_api_version('1.30')
def test_create_with_mounts(self):
mount = docker.types.Mount(
@ -569,9 +563,6 @@ class VolumeBindTest(BaseAPIIntegrationTest):
inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, True)
@pytest.mark.xfail(
IS_WINDOWS_PLATFORM, reason='Test not designed for Windows platform'
)
@requires_api_version('1.30')
def test_create_with_mounts_ro(self):
mount = docker.types.Mount(
@ -1116,9 +1107,7 @@ class ContainerTopTest(BaseAPIIntegrationTest):
self.client.start(container)
res = self.client.top(container)
if IS_WINDOWS_PLATFORM:
assert res['Titles'] == ['PID', 'USER', 'TIME', 'COMMAND']
else:
if not IS_WINDOWS_PLATFORM:
assert res['Titles'] == [
'UID', 'PID', 'PPID', 'C', 'STIME', 'TTY', 'TIME', 'CMD'
]

View File

@ -135,7 +135,7 @@ class PluginTest(BaseAPIIntegrationTest):
def test_create_plugin(self):
plugin_data_dir = os.path.join(
os.path.dirname(__file__), 'testdata/dummy-plugin'
os.path.dirname(__file__), os.path.join('testdata', 'dummy-plugin')
)
assert self.client.create_plugin(
'docker-sdk-py/dummy', plugin_data_dir

View File

@ -36,6 +36,9 @@ class ContainerCollectionTest(BaseIntegrationTest):
with pytest.raises(docker.errors.ImageNotFound):
client.containers.run("dockerpytest_does_not_exist")
@pytest.mark.skipif(
docker.constants.IS_WINDOWS_PLATFORM, reason="host mounts on Windows"
)
def test_run_with_volume(self):
client = docker.from_env(version=TEST_API_VERSION)
path = tempfile.mkdtemp()

View File

@ -44,7 +44,7 @@ def response(status_code=200, content='', headers=None, reason=None, elapsed=0,
return res
def fake_resolve_authconfig(authconfig, registry=None):
def fake_resolve_authconfig(authconfig, registry=None, *args, **kwargs):
return None
@ -365,7 +365,7 @@ class DockerApiTest(BaseAPIClientTest):
assert result == content
class StreamTest(unittest.TestCase):
class UnixSocketStreamTest(unittest.TestCase):
def setUp(self):
socket_dir = tempfile.mkdtemp()
self.build_context = tempfile.mkdtemp()
@ -462,7 +462,61 @@ class StreamTest(unittest.TestCase):
raise e
assert list(stream) == [
str(i).encode() for i in range(50)]
str(i).encode() for i in range(50)
]
class TCPSocketStreamTest(unittest.TestCase):
text_data = b'''
Now, those children out there, they're jumping through the
flames in the hope that the god of the fire will make them fruitful.
Really, you can't blame them. After all, what girl would not prefer the
child of a god to that of some acne-scarred artisan?
'''
def setUp(self):
self.server = six.moves.socketserver.ThreadingTCPServer(
('', 0), self.get_handler_class()
)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.setDaemon(True)
self.thread.start()
self.address = 'http://{}:{}'.format(
socket.gethostname(), self.server.server_address[1]
)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join()
def get_handler_class(self):
text_data = self.text_data
class Handler(six.moves.BaseHTTPServer.BaseHTTPRequestHandler, object):
def do_POST(self):
self.send_response(101)
self.send_header(
'Content-Type', 'application/vnd.docker.raw-stream'
)
self.send_header('Connection', 'Upgrade')
self.send_header('Upgrade', 'tcp')
self.end_headers()
self.wfile.flush()
time.sleep(0.2)
self.wfile.write(text_data)
self.wfile.flush()
return Handler
def test_read_from_socket(self):
with APIClient(base_url=self.address) as client:
resp = client._post(client._url('/dummy'), stream=True)
data = client._read_from_socket(resp, stream=True, tty=True)
results = b''.join(data)
assert results == self.text_data
class UserAgentTest(unittest.TestCase):