Skip to content

Commit dd85864

Browse files
mtsmfmshin-
authored andcommitted
Use config.json for detachKeys
Signed-off-by: Fumiaki Matsushima <[email protected]>
1 parent 2e8f1f7 commit dd85864

File tree

10 files changed

+294
-103
lines changed

10 files changed

+294
-103
lines changed

docker/api/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333
from ..tls import TLSConfig
3434
from ..transport import SSLAdapter, UnixAdapter
35-
from ..utils import utils, check_resource, update_headers
35+
from ..utils import utils, check_resource, update_headers, config
3636
from ..utils.socket import frames_iter, socket_raw_iter
3737
from ..utils.json_stream import json_stream
3838
try:
@@ -106,6 +106,7 @@ def __init__(self, base_url=None, version=None,
106106
self.headers['User-Agent'] = user_agent
107107

108108
self._auth_configs = auth.load_config()
109+
self._general_configs = config.load_general_config()
109110

110111
base_url = utils.parse_host(
111112
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)

docker/api/container.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def attach_socket(self, container, params=None, ws=False):
6666
container (str): The container to attach to.
6767
params (dict): Dictionary of request parameters (e.g. ``stdout``,
6868
``stderr``, ``stream``).
69+
For ``detachKeys``, ~/.docker/config.json is used by default.
6970
ws (bool): Use websockets instead of raw HTTP.
7071
7172
Raises:
@@ -79,6 +80,11 @@ def attach_socket(self, container, params=None, ws=False):
7980
'stream': 1
8081
}
8182

83+
if 'detachKeys' not in params \
84+
and 'detachKeys' in self._general_configs:
85+
86+
params['detachKeys'] = self._general_configs['detachKeys']
87+
8288
if ws:
8389
return self._attach_websocket(container, params)
8490

docker/api/exec_api.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class ExecApiMixin(object):
99
@utils.check_resource('container')
1010
def exec_create(self, container, cmd, stdout=True, stderr=True,
1111
stdin=False, tty=False, privileged=False, user='',
12-
environment=None, workdir=None):
12+
environment=None, workdir=None, detach_keys=None):
1313
"""
1414
Sets up an exec instance in a running container.
1515
@@ -27,6 +27,11 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
2727
the following format ``["PASSWORD=xxx"]`` or
2828
``{"PASSWORD": "xxx"}``.
2929
workdir (str): Path to working directory for this exec session
30+
detach_keys (str): Override the key sequence for detaching
31+
a container. Format is a single character `[a-Z]`
32+
or `ctrl-<value>` where `<value>` is one of:
33+
`a-z`, `@`, `^`, `[`, `,` or `_`.
34+
~/.docker/config.json is used by default.
3035
3136
Returns:
3237
(dict): A dictionary with an exec ``Id`` key.
@@ -74,6 +79,11 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
7479
)
7580
data['WorkingDir'] = workdir
7681

82+
if detach_keys:
83+
data['detachKeys'] = detach_keys
84+
elif 'detachKeys' in self._general_configs:
85+
data['detachKeys'] = self._general_configs['detachKeys']
86+
7787
url = self._url('/containers/{0}/exec', container)
7888
res = self._post_json(url, data=data)
7989
return self._result(res, True)

docker/auth.py

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import base64
22
import json
33
import logging
4-
import os
54

65
import dockerpycreds
76
import six
87

98
from . import errors
10-
from .constants import IS_WINDOWS_PLATFORM
9+
from .utils import config
1110

1211
INDEX_NAME = 'docker.io'
1312
INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME)
14-
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
15-
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
1613
TOKEN_USERNAME = '<token>'
1714

1815
log = logging.getLogger(__name__)
@@ -105,10 +102,10 @@ def resolve_authconfig(authconfig, registry=None):
105102
log.debug("Found {0}".format(repr(registry)))
106103
return authconfig[registry]
107104

108-
for key, config in six.iteritems(authconfig):
105+
for key, conf in six.iteritems(authconfig):
109106
if resolve_index_name(key) == registry:
110107
log.debug("Found {0}".format(repr(key)))
111-
return config
108+
return conf
112109

113110
log.debug("No entry found")
114111
return None
@@ -223,44 +220,6 @@ def parse_auth(entries, raise_on_error=False):
223220
return conf
224221

225222

226-
def find_config_file(config_path=None):
227-
paths = list(filter(None, [
228-
config_path, # 1
229-
config_path_from_environment(), # 2
230-
os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
231-
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
232-
]))
233-
234-
log.debug("Trying paths: {0}".format(repr(paths)))
235-
236-
for path in paths:
237-
if os.path.exists(path):
238-
log.debug("Found file at path: {0}".format(path))
239-
return path
240-
241-
log.debug("No config file found")
242-
243-
return None
244-
245-
246-
def config_path_from_environment():
247-
config_dir = os.environ.get('DOCKER_CONFIG')
248-
if not config_dir:
249-
return None
250-
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
251-
252-
253-
def home_dir():
254-
"""
255-
Get the user's home directory, using the same logic as the Docker Engine
256-
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
257-
"""
258-
if IS_WINDOWS_PLATFORM:
259-
return os.environ.get('USERPROFILE', '')
260-
else:
261-
return os.path.expanduser('~')
262-
263-
264223
def load_config(config_path=None):
265224
"""
266225
Loads authentication data from a Docker configuration file in the given
@@ -269,7 +228,7 @@ def load_config(config_path=None):
269228
explicit config_path parameter > DOCKER_CONFIG environment variable >
270229
~/.docker/config.json > ~/.dockercfg
271230
"""
272-
config_file = find_config_file(config_path)
231+
config_file = config.find_config_file(config_path)
273232

274233
if not config_file:
275234
return {}

docker/utils/config.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import json
2+
import logging
3+
import os
4+
5+
from ..constants import IS_WINDOWS_PLATFORM
6+
7+
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
8+
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
def find_config_file(config_path=None):
14+
paths = list(filter(None, [
15+
config_path, # 1
16+
config_path_from_environment(), # 2
17+
os.path.join(home_dir(), DOCKER_CONFIG_FILENAME), # 3
18+
os.path.join(home_dir(), LEGACY_DOCKER_CONFIG_FILENAME), # 4
19+
]))
20+
21+
log.debug("Trying paths: {0}".format(repr(paths)))
22+
23+
for path in paths:
24+
if os.path.exists(path):
25+
log.debug("Found file at path: {0}".format(path))
26+
return path
27+
28+
log.debug("No config file found")
29+
30+
return None
31+
32+
33+
def config_path_from_environment():
34+
config_dir = os.environ.get('DOCKER_CONFIG')
35+
if not config_dir:
36+
return None
37+
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
38+
39+
40+
def home_dir():
41+
"""
42+
Get the user's home directory, using the same logic as the Docker Engine
43+
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
44+
"""
45+
if IS_WINDOWS_PLATFORM:
46+
return os.environ.get('USERPROFILE', '')
47+
else:
48+
return os.path.expanduser('~')
49+
50+
51+
def load_general_config(config_path=None):
52+
config_file = find_config_file(config_path)
53+
54+
if not config_file:
55+
return {}
56+
57+
try:
58+
with open(config_file) as f:
59+
return json.load(f)
60+
except Exception as e:
61+
log.debug(e)
62+
pass
63+
64+
log.debug("All parsing attempts failed - returning empty config")
65+
return {}

tests/helpers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import tarfile
66
import tempfile
77
import time
8+
import re
9+
import socket
10+
import six
811

912
import docker
1013
import pytest
@@ -102,3 +105,22 @@ def force_leave_swarm(client):
102105

103106
def swarm_listen_addr():
104107
return '0.0.0.0:{0}'.format(random.randrange(10000, 25000))
108+
109+
110+
def assert_socket_closed_with_keys(sock, inputs):
111+
if six.PY3:
112+
sock = sock._sock
113+
114+
for i in inputs:
115+
sock.send(i)
116+
time.sleep(1)
117+
118+
with pytest.raises(socket.error):
119+
sock.send(b"make sure the socket is closed\n")
120+
121+
122+
def ctrl_with(char):
123+
if re.match('[a-z]', char):
124+
return chr(ord(char) - ord('a') + 1).encode('ascii')
125+
else:
126+
raise(Exception('char must be [a-z]'))

tests/integration/api_container_test.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import signal
34
import tempfile
45
from datetime import datetime
@@ -15,8 +16,9 @@
1516

1617
from .base import BUSYBOX, BaseAPIIntegrationTest
1718
from .. import helpers
18-
from ..helpers import requires_api_version
19-
import re
19+
from ..helpers import (
20+
requires_api_version, ctrl_with, assert_socket_closed_with_keys
21+
)
2022

2123

2224
class ListContainersTest(BaseAPIIntegrationTest):
@@ -1223,6 +1225,58 @@ def test_attach_no_stream(self):
12231225
output = self.client.attach(container, stream=False, logs=True)
12241226
assert output == 'hello\n'.encode(encoding='ascii')
12251227

1228+
def test_detach_with_default(self):
1229+
container = self.client.create_container(
1230+
BUSYBOX, '/bin/sh',
1231+
detach=True, stdin_open=True, tty=True
1232+
)
1233+
id = container['Id']
1234+
self.tmp_containers.append(id)
1235+
self.client.start(id)
1236+
1237+
sock = self.client.attach_socket(
1238+
container,
1239+
{'stdin': True, 'stream': True}
1240+
)
1241+
1242+
assert_socket_closed_with_keys(sock, [ctrl_with('p'), ctrl_with('q')])
1243+
1244+
def test_detach_with_config_file(self):
1245+
self.client._general_configs['detachKeys'] = 'ctrl-p'
1246+
1247+
container = self.client.create_container(
1248+
BUSYBOX, '/bin/sh',
1249+
detach=True, stdin_open=True, tty=True
1250+
)
1251+
id = container['Id']
1252+
self.tmp_containers.append(id)
1253+
self.client.start(id)
1254+
1255+
sock = self.client.attach_socket(
1256+
container,
1257+
{'stdin': True, 'stream': True}
1258+
)
1259+
1260+
assert_socket_closed_with_keys(sock, [ctrl_with('p')])
1261+
1262+
def test_detach_with_arg(self):
1263+
self.client._general_configs['detachKeys'] = 'ctrl-p'
1264+
1265+
container = self.client.create_container(
1266+
BUSYBOX, '/bin/sh',
1267+
detach=True, stdin_open=True, tty=True
1268+
)
1269+
id = container['Id']
1270+
self.tmp_containers.append(id)
1271+
self.client.start(id)
1272+
1273+
sock = self.client.attach_socket(
1274+
container,
1275+
{'stdin': True, 'stream': True, 'detachKeys': 'ctrl-x'}
1276+
)
1277+
1278+
assert_socket_closed_with_keys(sock, [ctrl_with('x')])
1279+
12261280

12271281
class PauseTest(BaseAPIIntegrationTest):
12281282
def test_pause_unpause(self):

tests/integration/api_exec_test.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from docker.utils.socket import read_exactly
33

44
from .base import BaseAPIIntegrationTest, BUSYBOX
5-
from ..helpers import requires_api_version
5+
from ..helpers import (
6+
requires_api_version, ctrl_with, assert_socket_closed_with_keys
7+
)
68

79

810
class ExecTest(BaseAPIIntegrationTest):
@@ -148,3 +150,44 @@ def test_exec_command_with_workdir(self):
148150
res = self.client.exec_create(container, 'pwd', workdir='/var/www')
149151
exec_log = self.client.exec_start(res)
150152
assert exec_log == b'/var/www\n'
153+
154+
def test_detach_with_default(self):
155+
container = self.client.create_container(BUSYBOX, 'cat',
156+
detach=True, stdin_open=True)
157+
id = container['Id']
158+
self.client.start(id)
159+
self.tmp_containers.append(id)
160+
161+
exec_id = self.client.exec_create(id, '/bin/sh', stdin=True, tty=True)
162+
sock = self.client.exec_start(exec_id, tty=True, socket=True)
163+
164+
assert_socket_closed_with_keys(sock, [ctrl_with('p'), ctrl_with('q')])
165+
166+
def test_detach_with_config_file(self):
167+
self.client._general_configs['detachKeys'] = 'ctrl-p'
168+
container = self.client.create_container(BUSYBOX, 'cat',
169+
detach=True, stdin_open=True)
170+
id = container['Id']
171+
self.client.start(id)
172+
self.tmp_containers.append(id)
173+
174+
exec_id = self.client.exec_create(id, '/bin/sh', stdin=True, tty=True)
175+
sock = self.client.exec_start(exec_id, tty=True, socket=True)
176+
177+
assert_socket_closed_with_keys(sock, [ctrl_with('p')])
178+
179+
def test_detach_with_arg(self):
180+
self.client._general_configs['detachKeys'] = 'ctrl-p'
181+
container = self.client.create_container(BUSYBOX, 'cat',
182+
detach=True, stdin_open=True)
183+
id = container['Id']
184+
self.client.start(id)
185+
self.tmp_containers.append(id)
186+
187+
exec_id = self.client.exec_create(
188+
id, '/bin/sh',
189+
stdin=True, tty=True, detach_keys='ctrl-x'
190+
)
191+
sock = self.client.exec_start(exec_id, tty=True, socket=True)
192+
193+
assert_socket_closed_with_keys(sock, [ctrl_with('x')])

0 commit comments

Comments
 (0)