Use config.json for detachKeys

Signed-off-by: Fumiaki Matsushima <mtsmfm@gmail.com>
This commit is contained in:
Fumiaki MATSUSHIMA 2017-12-01 02:40:13 +09:00 committed by Joffrey F
parent 2e8f1f798a
commit dd858648a0
10 changed files with 294 additions and 103 deletions

View File

@ -32,7 +32,7 @@ from ..errors import (
)
from ..tls import TLSConfig
from ..transport import SSLAdapter, UnixAdapter
from ..utils import utils, check_resource, update_headers
from ..utils import utils, check_resource, update_headers, config
from ..utils.socket import frames_iter, socket_raw_iter
from ..utils.json_stream import json_stream
try:
@ -106,6 +106,7 @@ class APIClient(
self.headers['User-Agent'] = user_agent
self._auth_configs = auth.load_config()
self._general_configs = config.load_general_config()
base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)

View File

@ -66,6 +66,7 @@ class ContainerApiMixin(object):
container (str): The container to attach to.
params (dict): Dictionary of request parameters (e.g. ``stdout``,
``stderr``, ``stream``).
For ``detachKeys``, ~/.docker/config.json is used by default.
ws (bool): Use websockets instead of raw HTTP.
Raises:
@ -79,6 +80,11 @@ class ContainerApiMixin(object):
'stream': 1
}
if 'detachKeys' not in params \
and 'detachKeys' in self._general_configs:
params['detachKeys'] = self._general_configs['detachKeys']
if ws:
return self._attach_websocket(container, params)

View File

@ -9,7 +9,7 @@ class ExecApiMixin(object):
@utils.check_resource('container')
def exec_create(self, container, cmd, stdout=True, stderr=True,
stdin=False, tty=False, privileged=False, user='',
environment=None, workdir=None):
environment=None, workdir=None, detach_keys=None):
"""
Sets up an exec instance in a running container.
@ -27,6 +27,11 @@ class ExecApiMixin(object):
the following format ``["PASSWORD=xxx"]`` or
``{"PASSWORD": "xxx"}``.
workdir (str): Path to working directory for this exec session
detach_keys (str): Override the key sequence for detaching
a container. Format is a single character `[a-Z]`
or `ctrl-<value>` where `<value>` is one of:
`a-z`, `@`, `^`, `[`, `,` or `_`.
~/.docker/config.json is used by default.
Returns:
(dict): A dictionary with an exec ``Id`` key.
@ -74,6 +79,11 @@ class ExecApiMixin(object):
)
data['WorkingDir'] = workdir
if detach_keys:
data['detachKeys'] = detach_keys
elif 'detachKeys' in self._general_configs:
data['detachKeys'] = self._general_configs['detachKeys']
url = self._url('/containers/{0}/exec', container)
res = self._post_json(url, data=data)
return self._result(res, True)

View File

@ -1,18 +1,15 @@
import base64
import json
import logging
import os
import dockerpycreds
import six
from . import errors
from .constants import IS_WINDOWS_PLATFORM
from .utils import config
INDEX_NAME = 'docker.io'
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME)
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
TOKEN_USERNAME = '<token>'
log = logging.getLogger(__name__)
@ -105,10 +102,10 @@ def resolve_authconfig(authconfig, registry=None):
log.debug("Found {0}".format(repr(registry)))
return authconfig[registry]
for key, config in six.iteritems(authconfig):
for key, conf in six.iteritems(authconfig):
if resolve_index_name(key) == registry:
log.debug("Found {0}".format(repr(key)))
return config
return conf
log.debug("No entry found")
return None
@ -223,44 +220,6 @@ def parse_auth(entries, raise_on_error=False):
return conf
def find_config_file(config_path=None):
paths = list(filter(None, [
config_path, # 1
config_path_from_environment(), # 2
os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
]))
log.debug("Trying paths: {0}".format(repr(paths)))
for path in paths:
if os.path.exists(path):
log.debug("Found file at path: {0}".format(path))
return path
log.debug("No config file found")
return None
def config_path_from_environment():
config_dir = os.environ.get('DOCKER_CONFIG')
if not config_dir:
return None
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
def home_dir():
"""
Get the user's home directory, using the same logic as the Docker Engine
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
"""
if IS_WINDOWS_PLATFORM:
return os.environ.get('USERPROFILE', '')
else:
return os.path.expanduser('~')
def load_config(config_path=None):
"""
Loads authentication data from a Docker configuration file in the given
@ -269,7 +228,7 @@ def load_config(config_path=None):
explicit config_path parameter > DOCKER_CONFIG environment variable >
~/.docker/config.json > ~/.dockercfg
"""
config_file = find_config_file(config_path)
config_file = config.find_config_file(config_path)
if not config_file:
return {}

65
docker/utils/config.py Normal file
View File

@ -0,0 +1,65 @@
import json
import logging
import os
from ..constants import IS_WINDOWS_PLATFORM
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
log = logging.getLogger(__name__)
def find_config_file(config_path=None):
paths = list(filter(None, [
config_path, # 1
config_path_from_environment(), # 2
os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
]))
log.debug("Trying paths: {0}".format(repr(paths)))
for path in paths:
if os.path.exists(path):
log.debug("Found file at path: {0}".format(path))
return path
log.debug("No config file found")
return None
def config_path_from_environment():
config_dir = os.environ.get('DOCKER_CONFIG')
if not config_dir:
return None
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
def home_dir():
"""
Get the user's home directory, using the same logic as the Docker Engine
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
"""
if IS_WINDOWS_PLATFORM:
return os.environ.get('USERPROFILE', '')
else:
return os.path.expanduser('~')
def load_general_config(config_path=None):
config_file = find_config_file(config_path)
if not config_file:
return {}
try:
with open(config_file) as f:
return json.load(f)
except Exception as e:
log.debug(e)
pass
log.debug("All parsing attempts failed - returning empty config")
return {}

View File

@ -5,6 +5,9 @@ import random
import tarfile
import tempfile
import time
import re
import socket
import six
import docker
import pytest
@ -102,3 +105,22 @@ def force_leave_swarm(client):
def swarm_listen_addr():
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000))
def assert_socket_closed_with_keys(sock, inputs):
if six.PY3:
sock = sock._sock
for i in inputs:
sock.send(i)
time.sleep(1)
with pytest.raises(socket.error):
sock.send(b"make sure the socket is closed\n")
def ctrl_with(char):
if re.match('[a-z]', char):
return chr(ord(char) - ord('a') + 1).encode('ascii')
else:
raise(Exception('char must be [a-z]'))

View File

@ -1,4 +1,5 @@
import os
import re
import signal
import tempfile
from datetime import datetime
@ -15,8 +16,9 @@ import six
from .base import BUSYBOX, BaseAPIIntegrationTest
from .. import helpers
from ..helpers import requires_api_version
import re
from ..helpers import (
requires_api_version, ctrl_with, assert_socket_closed_with_keys
)
class ListContainersTest(BaseAPIIntegrationTest):
@ -1223,6 +1225,58 @@ class AttachContainerTest(BaseAPIIntegrationTest):
output = self.client.attach(container, stream=False, logs=True)
assert output == 'hello\n'.encode(encoding='ascii')
def test_detach_with_default(self):
container = self.client.create_container(
BUSYBOX, '/bin/sh',
detach=True, stdin_open=True, tty=True
)
id = container['Id']
self.tmp_containers.append(id)
self.client.start(id)
sock = self.client.attach_socket(
container,
{'stdin': True, 'stream': True}
)
assert_socket_closed_with_keys(sock, [ctrl_with('p'), ctrl_with('q')])
def test_detach_with_config_file(self):
self.client._general_configs['detachKeys'] = 'ctrl-p'
container = self.client.create_container(
BUSYBOX, '/bin/sh',
detach=True, stdin_open=True, tty=True
)
id = container['Id']
self.tmp_containers.append(id)
self.client.start(id)
sock = self.client.attach_socket(
container,
{'stdin': True, 'stream': True}
)
assert_socket_closed_with_keys(sock, [ctrl_with('p')])
def test_detach_with_arg(self):
self.client._general_configs['detachKeys'] = 'ctrl-p'
container = self.client.create_container(
BUSYBOX, '/bin/sh',
detach=True, stdin_open=True, tty=True
)
id = container['Id']
self.tmp_containers.append(id)
self.client.start(id)
sock = self.client.attach_socket(
container,
{'stdin': True, 'stream': True, 'detachKeys': 'ctrl-x'}
)
assert_socket_closed_with_keys(sock, [ctrl_with('x')])
class PauseTest(BaseAPIIntegrationTest):
def test_pause_unpause(self):

View File

@ -2,7 +2,9 @@ from docker.utils.socket import next_frame_size
from docker.utils.socket import read_exactly
from .base import BaseAPIIntegrationTest, BUSYBOX
from ..helpers import requires_api_version
from ..helpers import (
requires_api_version, ctrl_with, assert_socket_closed_with_keys
)
class ExecTest(BaseAPIIntegrationTest):
@ -148,3 +150,44 @@ class ExecTest(BaseAPIIntegrationTest):
res = self.client.exec_create(container, 'pwd', workdir='/var/www')
exec_log = self.client.exec_start(res)
assert exec_log == b'/var/www\n'
def test_detach_with_default(self):
container = self.client.create_container(BUSYBOX, 'cat',
detach=True, stdin_open=True)
id = container['Id']
self.client.start(id)
self.tmp_containers.append(id)
exec_id = self.client.exec_create(id, '/bin/sh', stdin=True, tty=True)
sock = self.client.exec_start(exec_id, tty=True, socket=True)
assert_socket_closed_with_keys(sock, [ctrl_with('p'), ctrl_with('q')])
def test_detach_with_config_file(self):
self.client._general_configs['detachKeys'] = 'ctrl-p'
container = self.client.create_container(BUSYBOX, 'cat',
detach=True, stdin_open=True)
id = container['Id']
self.client.start(id)
self.tmp_containers.append(id)
exec_id = self.client.exec_create(id, '/bin/sh', stdin=True, tty=True)
sock = self.client.exec_start(exec_id, tty=True, socket=True)
assert_socket_closed_with_keys(sock, [ctrl_with('p')])
def test_detach_with_arg(self):
self.client._general_configs['detachKeys'] = 'ctrl-p'
container = self.client.create_container(BUSYBOX, 'cat',
detach=True, stdin_open=True)
id = container['Id']
self.client.start(id)
self.tmp_containers.append(id)
exec_id = self.client.exec_create(
id, '/bin/sh',
stdin=True, tty=True, detach_keys='ctrl-x'
)
sock = self.client.exec_start(exec_id, tty=True, socket=True)
assert_socket_closed_with_keys(sock, [ctrl_with('x')])

View File

@ -9,9 +9,6 @@ import shutil
import tempfile
import unittest
from py.test import ensuretemp
from pytest import mark
from docker import auth, errors
import pytest
@ -263,56 +260,6 @@ class CredStoreTest(unittest.TestCase):
) == 'truesecret'
class FindConfigFileTest(unittest.TestCase):
def tmpdir(self, name):
tmpdir = ensuretemp(name)
self.addCleanup(tmpdir.remove)
return tmpdir
def test_find_config_fallback(self):
tmpdir = self.tmpdir('test_find_config_fallback')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert auth.find_config_file() is None
def test_find_config_from_explicit_path(self):
tmpdir = self.tmpdir('test_find_config_from_explicit_path')
config_path = tmpdir.ensure('my-config-file.json')
assert auth.find_config_file(str(config_path)) == str(config_path)
def test_find_config_from_environment(self):
tmpdir = self.tmpdir('test_find_config_from_environment')
config_path = tmpdir.ensure('config.json')
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': str(tmpdir)}):
assert auth.find_config_file() == str(config_path)
@mark.skipif("sys.platform == 'win32'")
def test_find_config_from_home_posix(self):
tmpdir = self.tmpdir('test_find_config_from_home_posix')
config_path = tmpdir.ensure('.docker', 'config.json')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert auth.find_config_file() == str(config_path)
@mark.skipif("sys.platform == 'win32'")
def test_find_config_from_home_legacy_name(self):
tmpdir = self.tmpdir('test_find_config_from_home_legacy_name')
config_path = tmpdir.ensure('.dockercfg')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert auth.find_config_file() == str(config_path)
@mark.skipif("sys.platform != 'win32'")
def test_find_config_from_home_windows(self):
tmpdir = self.tmpdir('test_find_config_from_home_windows')
config_path = tmpdir.ensure('.docker', 'config.json')
with mock.patch.dict(os.environ, {'USERPROFILE': str(tmpdir)}):
assert auth.find_config_file() == str(config_path)
class LoadConfigTest(unittest.TestCase):
def test_load_config_no_file(self):
folder = tempfile.mkdtemp()

View File

@ -0,0 +1,84 @@
import os
import unittest
import shutil
import tempfile
import json
from py.test import ensuretemp
from pytest import mark
from docker.utils import config
try:
from unittest import mock
except ImportError:
import mock
class FindConfigFileTest(unittest.TestCase):
def tmpdir(self, name):
tmpdir = ensuretemp(name)
self.addCleanup(tmpdir.remove)
return tmpdir
def test_find_config_fallback(self):
tmpdir = self.tmpdir('test_find_config_fallback')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert config.find_config_file() is None
def test_find_config_from_explicit_path(self):
tmpdir = self.tmpdir('test_find_config_from_explicit_path')
config_path = tmpdir.ensure('my-config-file.json')
assert config.find_config_file(str(config_path)) == str(config_path)
def test_find_config_from_environment(self):
tmpdir = self.tmpdir('test_find_config_from_environment')
config_path = tmpdir.ensure('config.json')
with mock.patch.dict(os.environ, {'DOCKER_CONFIG': str(tmpdir)}):
assert config.find_config_file() == str(config_path)
@mark.skipif("sys.platform == 'win32'")
def test_find_config_from_home_posix(self):
tmpdir = self.tmpdir('test_find_config_from_home_posix')
config_path = tmpdir.ensure('.docker', 'config.json')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert config.find_config_file() == str(config_path)
@mark.skipif("sys.platform == 'win32'")
def test_find_config_from_home_legacy_name(self):
tmpdir = self.tmpdir('test_find_config_from_home_legacy_name')
config_path = tmpdir.ensure('.dockercfg')
with mock.patch.dict(os.environ, {'HOME': str(tmpdir)}):
assert config.find_config_file() == str(config_path)
@mark.skipif("sys.platform != 'win32'")
def test_find_config_from_home_windows(self):
tmpdir = self.tmpdir('test_find_config_from_home_windows')
config_path = tmpdir.ensure('.docker', 'config.json')
with mock.patch.dict(os.environ, {'USERPROFILE': str(tmpdir)}):
assert config.find_config_file() == str(config_path)
class LoadConfigTest(unittest.TestCase):
def test_load_config_no_file(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
cfg = config.load_general_config(folder)
self.assertTrue(cfg is not None)
def test_load_config(self):
folder = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, folder)
dockercfg_path = os.path.join(folder, '.dockercfg')
cfg = {
'detachKeys': 'ctrl-q, ctrl-u, ctrl-i'
}
with open(dockercfg_path, 'w') as f:
json.dump(cfg, f)
self.assertEqual(config.load_general_config(dockercfg_path), cfg)