Skip to content

Commit 5455c04

Browse files
authored
Merge pull request #2219 from docker/2199-proxy-support
Support using proxy values from config
2 parents 4ca4e94 + 65bebc0 commit 5455c04

File tree

11 files changed

+292
-9
lines changed

11 files changed

+292
-9
lines changed

docker/api/build.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
1919
forcerm=False, dockerfile=None, container_limits=None,
2020
decode=False, buildargs=None, gzip=False, shmsize=None,
2121
labels=None, cache_from=None, target=None, network_mode=None,
22-
squash=None, extra_hosts=None, platform=None, isolation=None):
22+
squash=None, extra_hosts=None, platform=None, isolation=None,
23+
use_config_proxy=False):
2324
"""
2425
Similar to the ``docker build`` command. Either ``path`` or ``fileobj``
2526
needs to be set. ``path`` can be a local path (to a directory
@@ -103,6 +104,10 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
103104
platform (str): Platform in the format ``os[/arch[/variant]]``
104105
isolation (str): Isolation technology used during build.
105106
Default: `None`.
107+
use_config_proxy (bool): If ``True``, and if the docker client
108+
configuration file (``~/.docker/config.json`` by default)
109+
contains a proxy configuration, the corresponding environment
110+
variables will be set in the container being built.
106111
107112
Returns:
108113
A generator for the build output.
@@ -168,6 +173,10 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
168173
}
169174
params.update(container_limits)
170175

176+
if use_config_proxy:
177+
proxy_args = self._proxy_configs.get_environment()
178+
for k, v in proxy_args.items():
179+
buildargs.setdefault(k, v)
171180
if buildargs:
172181
params.update({'buildargs': json.dumps(buildargs)})
173182

docker/api/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from ..utils import utils, check_resource, update_headers, config
3535
from ..utils.socket import frames_iter, consume_socket_output, demux_adaptor
3636
from ..utils.json_stream import json_stream
37+
from ..utils.proxy import ProxyConfig
3738
try:
3839
from ..transport import NpipeAdapter
3940
except ImportError:
@@ -114,6 +115,15 @@ def __init__(self, base_url=None, version=None,
114115
self.headers['User-Agent'] = user_agent
115116

116117
self._general_configs = config.load_general_config()
118+
119+
proxy_config = self._general_configs.get('proxies', {})
120+
try:
121+
proxies = proxy_config[base_url]
122+
except KeyError:
123+
proxies = proxy_config.get('default', {})
124+
125+
self._proxy_configs = ProxyConfig.from_dict(proxies)
126+
117127
self._auth_configs = auth.load_config(
118128
config_dict=self._general_configs, credstore_env=credstore_env,
119129
)

docker/api/container.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ def create_container(self, image, command=None, hostname=None, user=None,
221221
working_dir=None, domainname=None, host_config=None,
222222
mac_address=None, labels=None, stop_signal=None,
223223
networking_config=None, healthcheck=None,
224-
stop_timeout=None, runtime=None):
224+
stop_timeout=None, runtime=None,
225+
use_config_proxy=False):
225226
"""
226227
Creates a container. Parameters are similar to those for the ``docker
227228
run`` command except it doesn't support the attach options (``-a``).
@@ -390,6 +391,10 @@ def create_container(self, image, command=None, hostname=None, user=None,
390391
runtime (str): Runtime to use with this container.
391392
healthcheck (dict): Specify a test to perform to check that the
392393
container is healthy.
394+
use_config_proxy (bool): If ``True``, and if the docker client
395+
configuration file (``~/.docker/config.json`` by default)
396+
contains a proxy configuration, the corresponding environment
397+
variables will be set in the container being created.
393398
394399
Returns:
395400
A dictionary with an image 'Id' key and a 'Warnings' key.
@@ -403,6 +408,14 @@ def create_container(self, image, command=None, hostname=None, user=None,
403408
if isinstance(volumes, six.string_types):
404409
volumes = [volumes, ]
405410

411+
if isinstance(environment, dict):
412+
environment = utils.utils.format_environment(environment)
413+
414+
if use_config_proxy:
415+
environment = self._proxy_configs.inject_proxy_environment(
416+
environment
417+
)
418+
406419
config = self.create_container_config(
407420
image, command, hostname, user, detach, stdin_open, tty,
408421
ports, environment, volumes,

docker/api/exec_api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ class ExecApiMixin(object):
88
@utils.check_resource('container')
99
def exec_create(self, container, cmd, stdout=True, stderr=True,
1010
stdin=False, tty=False, privileged=False, user='',
11-
environment=None, workdir=None, detach_keys=None):
11+
environment=None, workdir=None, detach_keys=None,
12+
use_config_proxy=False):
1213
"""
1314
Sets up an exec instance in a running container.
1415
@@ -31,6 +32,10 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
3132
or `ctrl-<value>` where `<value>` is one of:
3233
`a-z`, `@`, `^`, `[`, `,` or `_`.
3334
~/.docker/config.json is used by default.
35+
use_config_proxy (bool): If ``True``, and if the docker client
36+
configuration file (``~/.docker/config.json`` by default)
37+
contains a proxy configuration, the corresponding environment
38+
variables will be set in the container being created.
3439
3540
Returns:
3641
(dict): A dictionary with an exec ``Id`` key.
@@ -50,6 +55,9 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
5055

5156
if isinstance(environment, dict):
5257
environment = utils.utils.format_environment(environment)
58+
if use_config_proxy:
59+
environment = \
60+
self._proxy_configs.inject_proxy_environment(environment)
5361

5462
data = {
5563
'Container': container,

docker/models/containers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ def diff(self):
144144

145145
def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
146146
privileged=False, user='', detach=False, stream=False,
147-
socket=False, environment=None, workdir=None, demux=False):
147+
socket=False, environment=None, workdir=None, demux=False,
148+
use_config_proxy=False):
148149
"""
149150
Run a command inside this container. Similar to
150151
``docker exec``.
@@ -167,6 +168,10 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
167168
``{"PASSWORD": "xxx"}``.
168169
workdir (str): Path to working directory for this exec session
169170
demux (bool): Return stdout and stderr separately
171+
use_config_proxy (bool): If ``True``, and if the docker client
172+
configuration file (``~/.docker/config.json`` by default)
173+
contains a proxy configuration, the corresponding environment
174+
variables will be set in the command's environment.
170175
171176
Returns:
172177
(ExecResult): A tuple of (exit_code, output)
@@ -185,7 +190,7 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
185190
resp = self.client.api.exec_create(
186191
self.id, cmd, stdout=stdout, stderr=stderr, stdin=stdin, tty=tty,
187192
privileged=privileged, user=user, environment=environment,
188-
workdir=workdir
193+
workdir=workdir, use_config_proxy=use_config_proxy,
189194
)
190195
exec_output = self.client.api.exec_start(
191196
resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket,

docker/utils/proxy.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from .utils import format_environment
2+
3+
4+
class ProxyConfig(dict):
5+
'''
6+
Hold the client's proxy configuration
7+
'''
8+
@property
9+
def http(self):
10+
return self.get('http')
11+
12+
@property
13+
def https(self):
14+
return self.get('https')
15+
16+
@property
17+
def ftp(self):
18+
return self.get('ftp')
19+
20+
@property
21+
def no_proxy(self):
22+
return self.get('no_proxy')
23+
24+
@staticmethod
25+
def from_dict(config):
26+
'''
27+
Instantiate a new ProxyConfig from a dictionary that represents a
28+
client configuration, as described in `the documentation`_.
29+
30+
.. _the documentation:
31+
https://docs.docker.com/network/proxy/#configure-the-docker-client
32+
'''
33+
return ProxyConfig(
34+
http=config.get('httpProxy'),
35+
https=config.get('httpsProxy'),
36+
ftp=config.get('ftpProxy'),
37+
no_proxy=config.get('noProxy'),
38+
)
39+
40+
def get_environment(self):
41+
'''
42+
Return a dictionary representing the environment variables used to
43+
set the proxy settings.
44+
'''
45+
env = {}
46+
if self.http:
47+
env['http_proxy'] = env['HTTP_PROXY'] = self.http
48+
if self.https:
49+
env['https_proxy'] = env['HTTPS_PROXY'] = self.https
50+
if self.ftp:
51+
env['ftp_proxy'] = env['FTP_PROXY'] = self.ftp
52+
if self.no_proxy:
53+
env['no_proxy'] = env['NO_PROXY'] = self.no_proxy
54+
return env
55+
56+
def inject_proxy_environment(self, environment):
57+
'''
58+
Given a list of strings representing environment variables, prepend the
59+
environment variables corresponding to the proxy settings.
60+
'''
61+
if not self:
62+
return environment
63+
64+
proxy_env = format_environment(self.get_environment())
65+
if not environment:
66+
return proxy_env
67+
# It is important to prepend our variables, because we want the
68+
# variables defined in "environment" to take precedence.
69+
return proxy_env + environment
70+
71+
def __str__(self):
72+
return 'ProxyConfig(http={}, https={}, ftp={}, no_proxy={})'.format(
73+
self.http, self.https, self.ftp, self.no_proxy)

tests/integration/api_build_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import tempfile
55

66
from docker import errors
7+
from docker.utils.proxy import ProxyConfig
78

89
import pytest
910
import six
@@ -13,6 +14,48 @@
1314

1415

1516
class BuildTest(BaseAPIIntegrationTest):
17+
def test_build_with_proxy(self):
18+
self.client._proxy_configs = ProxyConfig(
19+
ftp='a', http='b', https='c', no_proxy='d'
20+
)
21+
22+
script = io.BytesIO('\n'.join([
23+
'FROM busybox',
24+
'RUN env | grep "FTP_PROXY=a"',
25+
'RUN env | grep "ftp_proxy=a"',
26+
'RUN env | grep "HTTP_PROXY=b"',
27+
'RUN env | grep "http_proxy=b"',
28+
'RUN env | grep "HTTPS_PROXY=c"',
29+
'RUN env | grep "https_proxy=c"',
30+
'RUN env | grep "NO_PROXY=d"',
31+
'RUN env | grep "no_proxy=d"',
32+
]).encode('ascii'))
33+
34+
self.client.build(fileobj=script, decode=True)
35+
36+
def test_build_with_proxy_and_buildargs(self):
37+
self.client._proxy_configs = ProxyConfig(
38+
ftp='a', http='b', https='c', no_proxy='d'
39+
)
40+
41+
script = io.BytesIO('\n'.join([
42+
'FROM busybox',
43+
'RUN env | grep "FTP_PROXY=XXX"',
44+
'RUN env | grep "ftp_proxy=xxx"',
45+
'RUN env | grep "HTTP_PROXY=b"',
46+
'RUN env | grep "http_proxy=b"',
47+
'RUN env | grep "HTTPS_PROXY=c"',
48+
'RUN env | grep "https_proxy=c"',
49+
'RUN env | grep "NO_PROXY=d"',
50+
'RUN env | grep "no_proxy=d"',
51+
]).encode('ascii'))
52+
53+
self.client.build(
54+
fileobj=script,
55+
decode=True,
56+
buildargs={'FTP_PROXY': 'XXX', 'ftp_proxy': 'xxx'}
57+
)
58+
1659
def test_build_streaming(self):
1760
script = io.BytesIO('\n'.join([
1861
'FROM busybox',

tests/integration/api_exec_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from docker.utils.socket import next_frame_header
22
from docker.utils.socket import read_exactly
3+
from docker.utils.proxy import ProxyConfig
34

45
from .base import BaseAPIIntegrationTest, BUSYBOX
56
from ..helpers import (
@@ -8,6 +9,45 @@
89

910

1011
class ExecTest(BaseAPIIntegrationTest):
12+
def test_execute_command_with_proxy_env(self):
13+
# Set a custom proxy config on the client
14+
self.client._proxy_configs = ProxyConfig(
15+
ftp='a', https='b', http='c', no_proxy='d'
16+
)
17+
18+
container = self.client.create_container(
19+
BUSYBOX, 'cat', detach=True, stdin_open=True,
20+
use_config_proxy=True,
21+
)
22+
self.client.start(container)
23+
self.tmp_containers.append(container)
24+
25+
cmd = 'sh -c "env | grep -i proxy"'
26+
27+
# First, just make sure the environment variables from the custom
28+
# config are set
29+
30+
res = self.client.exec_create(container, cmd=cmd)
31+
output = self.client.exec_start(res).decode('utf-8').split('\n')
32+
expected = [
33+
'ftp_proxy=a', 'https_proxy=b', 'http_proxy=c', 'no_proxy=d',
34+
'FTP_PROXY=a', 'HTTPS_PROXY=b', 'HTTP_PROXY=c', 'NO_PROXY=d'
35+
]
36+
for item in expected:
37+
assert item in output
38+
39+
# Overwrite some variables with a custom environment
40+
env = {'https_proxy': 'xxx', 'HTTPS_PROXY': 'XXX'}
41+
42+
res = self.client.exec_create(container, cmd=cmd, environment=env)
43+
output = self.client.exec_start(res).decode('utf-8').split('\n')
44+
expected = [
45+
'ftp_proxy=a', 'https_proxy=xxx', 'http_proxy=c', 'no_proxy=d',
46+
'FTP_PROXY=a', 'HTTPS_PROXY=XXX', 'HTTP_PROXY=c', 'NO_PROXY=d'
47+
]
48+
for item in expected:
49+
assert item in output
50+
1151
def test_execute_command(self):
1252
container = self.client.create_container(BUSYBOX, 'cat',
1353
detach=True, stdin_open=True)

tests/unit/api_test.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@ def setUp(self):
106106
)
107107
self.patcher.start()
108108
self.client = APIClient()
109-
# Force-clear authconfig to avoid tampering with the tests
110-
self.client._cfg = {'Configs': {}}
111109

112110
def tearDown(self):
113111
self.client.close()

tests/unit/models_containers_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def test_exec_run(self):
416416
client.api.exec_create.assert_called_with(
417417
FAKE_CONTAINER_ID, "echo hello world", stdout=True, stderr=True,
418418
stdin=False, tty=False, privileged=True, user='', environment=None,
419-
workdir=None
419+
workdir=None, use_config_proxy=False,
420420
)
421421
client.api.exec_start.assert_called_with(
422422
FAKE_EXEC_ID, detach=False, tty=False, stream=True, socket=False,
@@ -430,7 +430,7 @@ def test_exec_run_failure(self):
430430
client.api.exec_create.assert_called_with(
431431
FAKE_CONTAINER_ID, "docker ps", stdout=True, stderr=True,
432432
stdin=False, tty=False, privileged=True, user='', environment=None,
433-
workdir=None
433+
workdir=None, use_config_proxy=False,
434434
)
435435
client.api.exec_start.assert_called_with(
436436
FAKE_EXEC_ID, detach=False, tty=False, stream=False, socket=False,

0 commit comments

Comments
 (0)