mirror of https://github.com/docker/docker-py.git
				
				
				
			Merge pull request #2680 from aiordache/replace_paramiko
Shell out to local SSH client as alternative to a paramiko connection
This commit is contained in:
		
						commit
						e09b070575
					
				|  | @ -2,6 +2,10 @@ ARG PYTHON_VERSION=2.7 | |||
| 
 | ||||
| FROM python:${PYTHON_VERSION} | ||||
| 
 | ||||
| # Add SSH keys and set permissions | ||||
| COPY tests/ssh-keys /root/.ssh | ||||
| RUN chmod -R 600 /root/.ssh | ||||
| 
 | ||||
| RUN mkdir /src | ||||
| WORKDIR /src | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| def imageNameBase = "dockerbuildbot/docker-py" | ||||
| def imageNamePy2 | ||||
| def imageNamePy3 | ||||
| def imageDindSSH | ||||
| def images = [:] | ||||
| 
 | ||||
| def buildImage = { name, buildargs, pyTag -> | ||||
|  | @ -13,7 +14,7 @@ def buildImage = { name, buildargs, pyTag -> | |||
|     img = docker.build(name, buildargs) | ||||
|     img.push() | ||||
|   } | ||||
|   images[pyTag] = img.id | ||||
|   if (pyTag?.trim()) images[pyTag] = img.id | ||||
| } | ||||
| 
 | ||||
| def buildImages = { -> | ||||
|  | @ -23,7 +24,9 @@ def buildImages = { -> | |||
| 
 | ||||
|       imageNamePy2 = "${imageNameBase}:py2-${gitCommit()}" | ||||
|       imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}" | ||||
|       imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}" | ||||
| 
 | ||||
|       buildImage(imageDindSSH, "-f tests/Dockerfile-ssh-dind .", "") | ||||
|       buildImage(imageNamePy2, "-f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 .", "py2.7") | ||||
|       buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.7 .", "py3.7") | ||||
|     } | ||||
|  | @ -81,22 +84,37 @@ def runTests = { Map settings -> | |||
|         def testNetwork = "dpy-testnet-\$BUILD_NUMBER-\$EXECUTOR_NUMBER-${pythonVersion}-${dockerVersion}" | ||||
|         try { | ||||
|           sh """docker network create ${testNetwork}""" | ||||
|           sh """docker run -d --name  ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\ | ||||
|             docker:${dockerVersion}-dind dockerd -H tcp://0.0.0.0:2375 | ||||
|           sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\ | ||||
|             ${imageDindSSH} dockerd -H tcp://0.0.0.0:2375 | ||||
|           """ | ||||
|           sh """docker run \\ | ||||
|           sh """docker run --rm \\ | ||||
|             --name ${testContainerName} \\ | ||||
|             -e "DOCKER_HOST=tcp://${dindContainerName}:2375" \\ | ||||
|             -e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\ | ||||
|             --network ${testNetwork} \\ | ||||
|             --volumes-from ${dindContainerName} \\ | ||||
|             ${testImage} \\ | ||||
|             py.test -v -rxs --cov=docker tests/ | ||||
|             py.test -v -rxs --cov=docker --ignore=tests/ssh tests/ | ||||
|           """ | ||||
|           sh """docker stop ${dindContainerName}""" | ||||
|            | ||||
|           // start DIND container with SSH | ||||
|           sh """docker run --rm -d --name ${dindContainerName} -v /tmp --privileged --network ${testNetwork} \\ | ||||
|             ${imageDindSSH} dockerd --experimental""" | ||||
|           sh """docker exec ${dindContainerName} sh -c /usr/sbin/sshd """ | ||||
|           // run SSH tests only | ||||
|           sh """docker run --rm \\ | ||||
|             --name ${testContainerName} \\ | ||||
|             -e "DOCKER_HOST=ssh://${dindContainerName}:22" \\ | ||||
|             -e 'DOCKER_TEST_API_VERSION=${apiVersion}' \\ | ||||
|             --network ${testNetwork} \\ | ||||
|             --volumes-from ${dindContainerName} \\ | ||||
|             ${testImage} \\ | ||||
|             py.test -v -rxs --cov=docker tests/ssh | ||||
|           """ | ||||
|         } finally { | ||||
|           sh """ | ||||
|             docker stop ${dindContainerName} ${testContainerName} | ||||
|             docker rm -vf ${dindContainerName} ${testContainerName} | ||||
|             docker stop ${dindContainerName} | ||||
|             docker network rm ${testNetwork} | ||||
|           """ | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										33
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										33
									
								
								Makefile
								
								
								
								
							|  | @ -1,3 +1,6 @@ | |||
| TEST_API_VERSION ?= 1.39 | ||||
| TEST_ENGINE_VERSION ?= 19.03.13 | ||||
| 
 | ||||
| .PHONY: all | ||||
| all: test | ||||
| 
 | ||||
|  | @ -10,6 +13,10 @@ clean: | |||
| build: | ||||
| 	docker build -t docker-sdk-python -f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 --build-arg APT_MIRROR . | ||||
| 
 | ||||
| .PHONY: build-dind-ssh | ||||
| build-dind-ssh: | ||||
| 	docker build -t docker-dind-ssh -f tests/Dockerfile-ssh-dind --build-arg ENGINE_VERSION=${TEST_ENGINE_VERSION} --build-arg API_VERSION=${TEST_API_VERSION} --build-arg APT_MIRROR . | ||||
| 
 | ||||
| .PHONY: build-py3 | ||||
| build-py3: | ||||
| 	docker build -t docker-sdk-python3 -f tests/Dockerfile --build-arg APT_MIRROR . | ||||
|  | @ -41,9 +48,6 @@ integration-test: build | |||
| integration-test-py3: build-py3 | ||||
| 	docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file} | ||||
| 
 | ||||
| TEST_API_VERSION ?= 1.39 | ||||
| TEST_ENGINE_VERSION ?= 19.03.12 | ||||
| 
 | ||||
| .PHONY: setup-network | ||||
| setup-network: | ||||
| 	docker network inspect dpy-tests || docker network create dpy-tests | ||||
|  | @ -69,6 +73,29 @@ integration-dind-py3: build-py3 setup-network | |||
| 		--network dpy-tests docker-sdk-python3 py.test tests/integration/${file} | ||||
| 	docker rm -vf dpy-dind-py3 | ||||
| 
 | ||||
| .PHONY: integration-ssh-py2 | ||||
| integration-ssh-py2: build-dind-ssh build setup-network | ||||
| 	docker rm -vf dpy-dind-py2 || : | ||||
| 	docker run -d --network dpy-tests --name dpy-dind-py2 --privileged\
 | ||||
| 		docker-dind-ssh dockerd --experimental | ||||
| 	# start SSH daemon | ||||
| 	docker exec dpy-dind-py2 sh -c "/usr/sbin/sshd" | ||||
| 	docker run -t --rm --env="DOCKER_HOST=ssh://dpy-dind-py2" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
 | ||||
| 		--network dpy-tests docker-sdk-python py.test tests/ssh/${file} | ||||
| 	docker rm -vf dpy-dind-py2 | ||||
| 
 | ||||
| .PHONY: integration-ssh-py3 | ||||
| integration-ssh-py3: build-dind-ssh build-py3 setup-network | ||||
| 	docker rm -vf dpy-dind-py3 || : | ||||
| 	docker run -d --network dpy-tests --name dpy-dind-py3 --privileged\
 | ||||
| 		docker-dind-ssh dockerd --experimental | ||||
| 	# start SSH daemon | ||||
| 	docker exec dpy-dind-py3 sh -c "/usr/sbin/sshd" | ||||
| 	docker run -t --rm --env="DOCKER_HOST=ssh://dpy-dind-py3" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
 | ||||
| 		--network dpy-tests docker-sdk-python3 py.test tests/ssh/${file} | ||||
| 	docker rm -vf dpy-dind-py3 | ||||
| 
 | ||||
| 
 | ||||
| .PHONY: integration-dind-ssl | ||||
| integration-dind-ssl: build-dind-certs build build-py3 | ||||
| 	docker rm -vf dpy-dind-certs dpy-dind-ssl || : | ||||
|  |  | |||
|  | @ -89,6 +89,9 @@ class APIClient( | |||
|         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. | ||||
|         use_ssh_client (bool): If set to `True`, an ssh connection is made | ||||
|             via shelling out to the ssh client. Ensure the ssh client is | ||||
|             installed and configured on the host. | ||||
|     """ | ||||
| 
 | ||||
|     __attrs__ = requests.Session.__attrs__ + ['_auth_configs', | ||||
|  | @ -100,7 +103,7 @@ class APIClient( | |||
|     def __init__(self, base_url=None, version=None, | ||||
|                  timeout=DEFAULT_TIMEOUT_SECONDS, tls=False, | ||||
|                  user_agent=DEFAULT_USER_AGENT, num_pools=None, | ||||
|                  credstore_env=None): | ||||
|                  credstore_env=None, use_ssh_client=False): | ||||
|         super(APIClient, self).__init__() | ||||
| 
 | ||||
|         if tls and not base_url: | ||||
|  | @ -161,7 +164,8 @@ class APIClient( | |||
|         elif base_url.startswith('ssh://'): | ||||
|             try: | ||||
|                 self._custom_adapter = SSHHTTPAdapter( | ||||
|                     base_url, timeout, pool_connections=num_pools | ||||
|                     base_url, timeout, pool_connections=num_pools, | ||||
|                     shell_out=use_ssh_client | ||||
|                 ) | ||||
|             except NameError: | ||||
|                 raise DockerException( | ||||
|  |  | |||
|  | @ -35,6 +35,9 @@ class DockerClient(object): | |||
|         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. | ||||
|         use_ssh_client (bool): If set to `True`, an ssh connection is made | ||||
|             via shelling out to the ssh client. Ensure the ssh client is | ||||
|             installed and configured on the host. | ||||
|     """ | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.api = APIClient(*args, **kwargs) | ||||
|  | @ -70,6 +73,9 @@ class DockerClient(object): | |||
|                 from. Default: the value of ``os.environ`` | ||||
|             credstore_env (dict): Override environment variables when calling | ||||
|                 the credential store process. | ||||
|             use_ssh_client (bool): If set to `True`, an ssh connection is | ||||
|                 made via shelling out to the ssh client. Ensure the ssh | ||||
|                 client is installed and configured on the host. | ||||
| 
 | ||||
|         Example: | ||||
| 
 | ||||
|  | @ -81,8 +87,12 @@ class DockerClient(object): | |||
|         """ | ||||
|         timeout = kwargs.pop('timeout', DEFAULT_TIMEOUT_SECONDS) | ||||
|         version = kwargs.pop('version', None) | ||||
|         use_ssh_client = kwargs.pop('use_ssh_client', False) | ||||
|         return cls( | ||||
|             timeout=timeout, version=version, **kwargs_from_env(**kwargs) | ||||
|             timeout=timeout, | ||||
|             version=version, | ||||
|             use_ssh_client=use_ssh_client, | ||||
|             **kwargs_from_env(**kwargs) | ||||
|         ) | ||||
| 
 | ||||
|     # Resources | ||||
|  |  | |||
|  | @ -1,8 +1,11 @@ | |||
| import io | ||||
| import paramiko | ||||
| import requests.adapters | ||||
| import six | ||||
| import logging | ||||
| import os | ||||
| import socket | ||||
| import subprocess | ||||
| 
 | ||||
| from docker.transport.basehttpadapter import BaseHTTPAdapter | ||||
| from .. import constants | ||||
|  | @ -20,33 +23,140 @@ except ImportError: | |||
| RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer | ||||
| 
 | ||||
| 
 | ||||
| def create_paramiko_client(base_url): | ||||
|     logging.getLogger("paramiko").setLevel(logging.WARNING) | ||||
|     ssh_client = paramiko.SSHClient() | ||||
|     base_url = six.moves.urllib_parse.urlparse(base_url) | ||||
|     ssh_params = { | ||||
|         "hostname": base_url.hostname, | ||||
|         "port": base_url.port, | ||||
|         "username": base_url.username | ||||
|         } | ||||
|     ssh_config_file = os.path.expanduser("~/.ssh/config") | ||||
|     if os.path.exists(ssh_config_file): | ||||
|         conf = paramiko.SSHConfig() | ||||
|         with open(ssh_config_file) as f: | ||||
|             conf.parse(f) | ||||
|         host_config = conf.lookup(base_url.hostname) | ||||
|         ssh_conf = host_config | ||||
|         if 'proxycommand' in host_config: | ||||
|             ssh_params["sock"] = paramiko.ProxyCommand( | ||||
|                 ssh_conf['proxycommand'] | ||||
|             ) | ||||
|         if 'hostname' in host_config: | ||||
|             ssh_params['hostname'] = host_config['hostname'] | ||||
|         if 'identityfile' in host_config: | ||||
|             ssh_params['key_filename'] = host_config['identityfile'] | ||||
|         if base_url.port is None and 'port' in host_config: | ||||
|             ssh_params['port'] = ssh_conf['port'] | ||||
|         if base_url.username is None and 'user' in host_config: | ||||
|             ssh_params['username'] = ssh_conf['user'] | ||||
| 
 | ||||
|     ssh_client.load_system_host_keys() | ||||
|     ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy()) | ||||
|     return ssh_client, ssh_params | ||||
| 
 | ||||
| 
 | ||||
| class SSHSocket(socket.socket): | ||||
|     def __init__(self, host): | ||||
|         super(SSHSocket, self).__init__( | ||||
|             socket.AF_INET, socket.SOCK_STREAM) | ||||
|         self.host = host | ||||
|         self.port = None | ||||
|         if ':' in host: | ||||
|             self.host, self.port = host.split(':') | ||||
|         self.proc = None | ||||
| 
 | ||||
|     def connect(self, **kwargs): | ||||
|         port = '' if not self.port else '-p {}'.format(self.port) | ||||
|         args = [ | ||||
|             'ssh', | ||||
|             '-q', | ||||
|             self.host, | ||||
|             port, | ||||
|             'docker system dial-stdio' | ||||
|         ] | ||||
|         self.proc = subprocess.Popen( | ||||
|             ' '.join(args), | ||||
|             shell=True, | ||||
|             stdout=subprocess.PIPE, | ||||
|             stdin=subprocess.PIPE) | ||||
| 
 | ||||
|     def _write(self, data): | ||||
|         if not self.proc or self.proc.stdin.closed: | ||||
|             raise Exception('SSH subprocess not initiated.' | ||||
|                             'connect() must be called first.') | ||||
|         written = self.proc.stdin.write(data) | ||||
|         self.proc.stdin.flush() | ||||
|         return written | ||||
| 
 | ||||
|     def sendall(self, data): | ||||
|         self._write(data) | ||||
| 
 | ||||
|     def send(self, data): | ||||
|         return self._write(data) | ||||
| 
 | ||||
|     def recv(self): | ||||
|         if not self.proc: | ||||
|             raise Exception('SSH subprocess not initiated.' | ||||
|                             'connect() must be called first.') | ||||
|         return self.proc.stdout.read() | ||||
| 
 | ||||
|     def makefile(self, mode): | ||||
|         if not self.proc or self.proc.stdout.closed: | ||||
|             buf = io.BytesIO() | ||||
|             buf.write(b'\n\n') | ||||
|             return buf | ||||
|         return self.proc.stdout | ||||
| 
 | ||||
|     def close(self): | ||||
|         if not self.proc or self.proc.stdin.closed: | ||||
|             return | ||||
|         self.proc.stdin.write(b'\n\n') | ||||
|         self.proc.stdin.flush() | ||||
|         self.proc.terminate() | ||||
| 
 | ||||
| 
 | ||||
| class SSHConnection(httplib.HTTPConnection, object): | ||||
|     def __init__(self, ssh_transport, timeout=60): | ||||
|     def __init__(self, ssh_transport=None, timeout=60, host=None): | ||||
|         super(SSHConnection, self).__init__( | ||||
|             'localhost', timeout=timeout | ||||
|         ) | ||||
|         self.ssh_transport = ssh_transport | ||||
|         self.timeout = timeout | ||||
|         self.host = host | ||||
| 
 | ||||
|     def connect(self): | ||||
|         sock = self.ssh_transport.open_session() | ||||
|         sock.settimeout(self.timeout) | ||||
|         sock.exec_command('docker system dial-stdio') | ||||
|         if self.ssh_transport: | ||||
|             sock = self.ssh_transport.open_session() | ||||
|             sock.settimeout(self.timeout) | ||||
|             sock.exec_command('docker system dial-stdio') | ||||
|         else: | ||||
|             sock = SSHSocket(self.host) | ||||
|             sock.settimeout(self.timeout) | ||||
|             sock.connect() | ||||
| 
 | ||||
|         self.sock = sock | ||||
| 
 | ||||
| 
 | ||||
| class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool): | ||||
|     scheme = 'ssh' | ||||
| 
 | ||||
|     def __init__(self, ssh_client, timeout=60, maxsize=10): | ||||
|     def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None): | ||||
|         super(SSHConnectionPool, self).__init__( | ||||
|             'localhost', timeout=timeout, maxsize=maxsize | ||||
|         ) | ||||
|         self.ssh_transport = ssh_client.get_transport() | ||||
|         self.ssh_transport = None | ||||
|         if ssh_client: | ||||
|             self.ssh_transport = ssh_client.get_transport() | ||||
|         self.timeout = timeout | ||||
|         self.host = host | ||||
|         self.port = None | ||||
|         if ':' in host: | ||||
|             self.host, self.port = host.split(':') | ||||
| 
 | ||||
|     def _new_conn(self): | ||||
|         return SSHConnection(self.ssh_transport, self.timeout) | ||||
|         return SSHConnection(self.ssh_transport, self.timeout, self.host) | ||||
| 
 | ||||
|     # When re-using connections, urllib3 calls fileno() on our | ||||
|     # SSH channel instance, quickly overloading our fd limit. To avoid this, | ||||
|  | @ -78,39 +188,14 @@ class SSHHTTPAdapter(BaseHTTPAdapter): | |||
|     ] | ||||
| 
 | ||||
|     def __init__(self, base_url, timeout=60, | ||||
|                  pool_connections=constants.DEFAULT_NUM_POOLS): | ||||
|         logging.getLogger("paramiko").setLevel(logging.WARNING) | ||||
|         self.ssh_client = paramiko.SSHClient() | ||||
|         base_url = six.moves.urllib_parse.urlparse(base_url) | ||||
|         self.ssh_params = { | ||||
|             "hostname": base_url.hostname, | ||||
|             "port": base_url.port, | ||||
|             "username": base_url.username | ||||
|             } | ||||
|         ssh_config_file = os.path.expanduser("~/.ssh/config") | ||||
|         if os.path.exists(ssh_config_file): | ||||
|             conf = paramiko.SSHConfig() | ||||
|             with open(ssh_config_file) as f: | ||||
|                 conf.parse(f) | ||||
|             host_config = conf.lookup(base_url.hostname) | ||||
|             self.ssh_conf = host_config | ||||
|             if 'proxycommand' in host_config: | ||||
|                 self.ssh_params["sock"] = paramiko.ProxyCommand( | ||||
|                     self.ssh_conf['proxycommand'] | ||||
|                 ) | ||||
|             if 'hostname' in host_config: | ||||
|                 self.ssh_params['hostname'] = host_config['hostname'] | ||||
|             if 'identityfile' in host_config: | ||||
|                 self.ssh_params['key_filename'] = host_config['identityfile'] | ||||
|             if base_url.port is None and 'port' in host_config: | ||||
|                 self.ssh_params['port'] = self.ssh_conf['port'] | ||||
|             if base_url.username is None and 'user' in host_config: | ||||
|                 self.ssh_params['username'] = self.ssh_conf['user'] | ||||
| 
 | ||||
|         self.ssh_client.load_system_host_keys() | ||||
|         self.ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy()) | ||||
| 
 | ||||
|         self._connect() | ||||
|                  pool_connections=constants.DEFAULT_NUM_POOLS, | ||||
|                  shell_out=True): | ||||
|         self.ssh_client = None | ||||
|         if not shell_out: | ||||
|             self.ssh_client, self.ssh_params = create_paramiko_client(base_url) | ||||
|             self._connect() | ||||
|         base_url = base_url.lstrip('ssh://') | ||||
|         self.host = base_url | ||||
|         self.timeout = timeout | ||||
|         self.pools = RecentlyUsedContainer( | ||||
|             pool_connections, dispose_func=lambda p: p.close() | ||||
|  | @ -118,7 +203,8 @@ class SSHHTTPAdapter(BaseHTTPAdapter): | |||
|         super(SSHHTTPAdapter, self).__init__() | ||||
| 
 | ||||
|     def _connect(self): | ||||
|         self.ssh_client.connect(**self.ssh_params) | ||||
|         if self.ssh_client: | ||||
|             self.ssh_client.connect(**self.ssh_params) | ||||
| 
 | ||||
|     def get_connection(self, url, proxies=None): | ||||
|         with self.pools.lock: | ||||
|  | @ -127,11 +213,13 @@ class SSHHTTPAdapter(BaseHTTPAdapter): | |||
|                 return pool | ||||
| 
 | ||||
|             # Connection is closed try a reconnect | ||||
|             if not self.ssh_client.get_transport(): | ||||
|             if self.ssh_client and not self.ssh_client.get_transport(): | ||||
|                 self._connect() | ||||
| 
 | ||||
|             pool = SSHConnectionPool( | ||||
|                 self.ssh_client, self.timeout | ||||
|                 ssh_client=self.ssh_client, | ||||
|                 timeout=self.timeout, | ||||
|                 host=self.host | ||||
|             ) | ||||
|             self.pools[url] = pool | ||||
| 
 | ||||
|  | @ -139,4 +227,5 @@ class SSHHTTPAdapter(BaseHTTPAdapter): | |||
| 
 | ||||
|     def close(self): | ||||
|         super(SSHHTTPAdapter, self).close() | ||||
|         self.ssh_client.close() | ||||
|         if self.ssh_client: | ||||
|             self.ssh_client.close() | ||||
|  |  | |||
|  | @ -6,10 +6,13 @@ ARG APT_MIRROR | |||
| RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \ | ||||
|     && sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list | ||||
| 
 | ||||
| RUN apt-get update && apt-get -y install \ | ||||
| RUN apt-get update && apt-get -y install --no-install-recommends \ | ||||
|     gnupg2 \ | ||||
|     pass \ | ||||
|     curl | ||||
|     pass | ||||
| 
 | ||||
| # Add SSH keys and set permissions | ||||
| COPY tests/ssh-keys /root/.ssh | ||||
| RUN chmod -R 600 /root/.ssh | ||||
| 
 | ||||
| COPY ./tests/gpg-keys /gpg-keys | ||||
| RUN gpg2 --import gpg-keys/secret | ||||
|  |  | |||
|  | @ -0,0 +1,23 @@ | |||
| ARG API_VERSION=1.39 | ||||
| ARG ENGINE_VERSION=19.03.12 | ||||
| 
 | ||||
| FROM docker:${ENGINE_VERSION}-dind | ||||
| 
 | ||||
| RUN apk add --no-cache \ | ||||
| 		openssh | ||||
| 
 | ||||
| # Add the keys and set permissions | ||||
| RUN ssh-keygen -A | ||||
| 
 | ||||
| # copy the test SSH config | ||||
| RUN echo "IgnoreUserKnownHosts yes" >> /etc/ssh/sshd_config && \ | ||||
|   echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config && \ | ||||
|   echo "PermitRootLogin yes" >> /etc/ssh/sshd_config | ||||
| 
 | ||||
| # set authorized keys for client paswordless connection | ||||
| COPY tests/ssh-keys/authorized_keys /root/.ssh/authorized_keys | ||||
| RUN chmod 600 /root/.ssh/authorized_keys | ||||
| 
 | ||||
| RUN echo "root:root" | chpasswd | ||||
| RUN ln -s /usr/local/bin/docker /usr/bin/docker | ||||
| EXPOSE 22 | ||||
|  | @ -339,7 +339,6 @@ class BuildTest(BaseAPIIntegrationTest): | |||
| 
 | ||||
|         assert self.client.inspect_image(img_name) | ||||
|         ctnr = self.run_container(img_name, 'cat /hosts-file') | ||||
|         self.tmp_containers.append(ctnr) | ||||
|         logs = self.client.logs(ctnr) | ||||
|         if six.PY3: | ||||
|             logs = logs.decode('utf-8') | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/BiXkbL9oEbE3PJv1S2p12XK5BHW3qQT5Rf+CYG0ATYyMPIVM6+IXVyf3QNxpnvPXvbPBQJCs0qHeuPwZy2Gsbt35QnmlgrczFPiXXosCD2N+wrcOQPZGuLjQyUUP2yJRVSTLpp8zk2F8w3laGIB3Jk1hUcMUExemKxQYk/L40b5rXKkarLk5awBuicjRStMrchPRHZ2n715TG+zSvf8tB/UHRXKYPqai/Je5eiH3yGUzCY4zn+uEoqAFb4V8lpIj8Rw3EXmCYVwG0vg+44QIQ2gJnIhTlcmxwkynvZn97nug4NLlGJQ+sDCnIvMapycHfGkNlBz3fFtu/ORsxPpZbTNg/9noa3Zf8OpIwvE/FHNPqDctGltwxEgQxj5fE34x0fYnF08tejAUJJCZE3YsGgNabsS4pD+kRhI83eFZvgj3Q1AeTK0V9bRM7jujcc9Rz+V9Gb5zYEHN/l8PxEVlj0OlURf9ZlknNQK8xRh597jDXTfVQKCMO/nRaWH2bq0=  | ||||
|  | @ -0,0 +1,3 @@ | |||
| Host * | ||||
| 	StrictHostKeyChecking no | ||||
| 	UserKnownHostsFile=/dev/null | ||||
|  | @ -0,0 +1,38 @@ | |||
| -----BEGIN OPENSSH PRIVATE KEY----- | ||||
| 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 | ||||
| qj4ZpdVMk2cCNcjwAAAAAB | ||||
| -----END OPENSSH PRIVATE KEY----- | ||||
|  | @ -0,0 +1 @@ | |||
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/BiXkbL9oEbE3PJv1S2p12XK5BHW3qQT5Rf+CYG0ATYyMPIVM6+IXVyf3QNxpnvPXvbPBQJCs0qHeuPwZy2Gsbt35QnmlgrczFPiXXosCD2N+wrcOQPZGuLjQyUUP2yJRVSTLpp8zk2F8w3laGIB3Jk1hUcMUExemKxQYk/L40b5rXKkarLk5awBuicjRStMrchPRHZ2n715TG+zSvf8tB/UHRXKYPqai/Je5eiH3yGUzCY4zn+uEoqAFb4V8lpIj8Rw3EXmCYVwG0vg+44QIQ2gJnIhTlcmxwkynvZn97nug4NLlGJQ+sDCnIvMapycHfGkNlBz3fFtu/ORsxPpZbTNg/9noa3Zf8OpIwvE/FHNPqDctGltwxEgQxj5fE34x0fYnF08tejAUJJCZE3YsGgNabsS4pD+kRhI83eFZvgj3Q1AeTK0V9bRM7jujcc9Rz+V9Gb5zYEHN/l8PxEVlj0OlURf9ZlknNQK8xRh597jDXTfVQKCMO/nRaWH2bq0=  | ||||
|  | @ -0,0 +1,595 @@ | |||
| import io | ||||
| import os | ||||
| import shutil | ||||
| import tempfile | ||||
| 
 | ||||
| from docker import errors | ||||
| from docker.utils.proxy import ProxyConfig | ||||
| 
 | ||||
| import pytest | ||||
| import six | ||||
| 
 | ||||
| from .base import BaseAPIIntegrationTest, TEST_IMG | ||||
| from ..helpers import random_name, requires_api_version, requires_experimental | ||||
| 
 | ||||
| 
 | ||||
| class BuildTest(BaseAPIIntegrationTest): | ||||
|     def test_build_with_proxy(self): | ||||
|         self.client._proxy_configs = ProxyConfig( | ||||
|             ftp='a', http='b', https='c', no_proxy='d' | ||||
|         ) | ||||
| 
 | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN env | grep "FTP_PROXY=a"', | ||||
|             'RUN env | grep "ftp_proxy=a"', | ||||
|             'RUN env | grep "HTTP_PROXY=b"', | ||||
|             'RUN env | grep "http_proxy=b"', | ||||
|             'RUN env | grep "HTTPS_PROXY=c"', | ||||
|             'RUN env | grep "https_proxy=c"', | ||||
|             'RUN env | grep "NO_PROXY=d"', | ||||
|             'RUN env | grep "no_proxy=d"', | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         self.client.build(fileobj=script, decode=True) | ||||
| 
 | ||||
|     def test_build_with_proxy_and_buildargs(self): | ||||
|         self.client._proxy_configs = ProxyConfig( | ||||
|             ftp='a', http='b', https='c', no_proxy='d' | ||||
|         ) | ||||
| 
 | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN env | grep "FTP_PROXY=XXX"', | ||||
|             'RUN env | grep "ftp_proxy=xxx"', | ||||
|             'RUN env | grep "HTTP_PROXY=b"', | ||||
|             'RUN env | grep "http_proxy=b"', | ||||
|             'RUN env | grep "HTTPS_PROXY=c"', | ||||
|             'RUN env | grep "https_proxy=c"', | ||||
|             'RUN env | grep "NO_PROXY=d"', | ||||
|             'RUN env | grep "no_proxy=d"', | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         self.client.build( | ||||
|             fileobj=script, | ||||
|             decode=True, | ||||
|             buildargs={'FTP_PROXY': 'XXX', 'ftp_proxy': 'xxx'} | ||||
|         ) | ||||
| 
 | ||||
|     def test_build_streaming(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN mkdir -p /tmp/test', | ||||
|             'EXPOSE 8080', | ||||
|             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' | ||||
|             ' /tmp/silence.tar.gz' | ||||
|         ]).encode('ascii')) | ||||
|         stream = self.client.build(fileobj=script, decode=True) | ||||
|         logs = [] | ||||
|         for chunk in stream: | ||||
|             logs.append(chunk) | ||||
|         assert len(logs) > 0 | ||||
| 
 | ||||
|     def test_build_from_stringio(self): | ||||
|         if six.PY3: | ||||
|             return | ||||
|         script = io.StringIO(six.text_type('\n').join([ | ||||
|             'FROM busybox', | ||||
|             'RUN mkdir -p /tmp/test', | ||||
|             'EXPOSE 8080', | ||||
|             'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' | ||||
|             ' /tmp/silence.tar.gz' | ||||
|         ])) | ||||
|         stream = self.client.build(fileobj=script) | ||||
|         logs = '' | ||||
|         for chunk in stream: | ||||
|             if six.PY3: | ||||
|                 chunk = chunk.decode('utf-8') | ||||
|             logs += chunk | ||||
|         assert logs != '' | ||||
| 
 | ||||
|     def test_build_with_dockerignore(self): | ||||
|         base_dir = tempfile.mkdtemp() | ||||
|         self.addCleanup(shutil.rmtree, base_dir) | ||||
| 
 | ||||
|         with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: | ||||
|             f.write("\n".join([ | ||||
|                 'FROM busybox', | ||||
|                 'ADD . /test', | ||||
|             ])) | ||||
| 
 | ||||
|         with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: | ||||
|             f.write("\n".join([ | ||||
|                 'ignored', | ||||
|                 'Dockerfile', | ||||
|                 '.dockerignore', | ||||
|                 '!ignored/subdir/excepted-file', | ||||
|                 '',  # empty line, | ||||
|                 '#*',  # comment line | ||||
|             ])) | ||||
| 
 | ||||
|         with open(os.path.join(base_dir, 'not-ignored'), 'w') as f: | ||||
|             f.write("this file should not be ignored") | ||||
| 
 | ||||
|         with open(os.path.join(base_dir, '#file.txt'), 'w') as f: | ||||
|             f.write('this file should not be ignored') | ||||
| 
 | ||||
|         subdir = os.path.join(base_dir, 'ignored', 'subdir') | ||||
|         os.makedirs(subdir) | ||||
|         with open(os.path.join(subdir, 'file'), 'w') as f: | ||||
|             f.write("this file should be ignored") | ||||
| 
 | ||||
|         with open(os.path.join(subdir, 'excepted-file'), 'w') as f: | ||||
|             f.write("this file should not be ignored") | ||||
| 
 | ||||
|         tag = 'docker-py-test-build-with-dockerignore' | ||||
|         stream = self.client.build( | ||||
|             path=base_dir, | ||||
|             tag=tag, | ||||
|         ) | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         c = self.client.create_container(tag, ['find', '/test', '-type', 'f']) | ||||
|         self.client.start(c) | ||||
|         self.client.wait(c) | ||||
|         logs = self.client.logs(c) | ||||
| 
 | ||||
|         if six.PY3: | ||||
|             logs = logs.decode('utf-8') | ||||
| 
 | ||||
|         assert sorted(list(filter(None, logs.split('\n')))) == sorted([ | ||||
|             '/test/#file.txt', | ||||
|             '/test/ignored/subdir/excepted-file', | ||||
|             '/test/not-ignored' | ||||
|         ]) | ||||
| 
 | ||||
|     def test_build_with_buildargs(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM scratch', | ||||
|             'ARG test', | ||||
|             'USER $test' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag='buildargs', buildargs={'test': 'OK'} | ||||
|         ) | ||||
|         self.tmp_imgs.append('buildargs') | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         info = self.client.inspect_image('buildargs') | ||||
|         assert info['Config']['User'] == 'OK' | ||||
| 
 | ||||
|     @requires_api_version('1.22') | ||||
|     def test_build_shmsize(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM scratch', | ||||
|             'CMD sh -c "echo \'Hello, World!\'"', | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         tag = 'shmsize' | ||||
|         shmsize = 134217728 | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag=tag, shmsize=shmsize | ||||
|         ) | ||||
|         self.tmp_imgs.append(tag) | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         # There is currently no way to get the shmsize | ||||
|         # that was used to build the image | ||||
| 
 | ||||
|     @requires_api_version('1.24') | ||||
|     def test_build_isolation(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM scratch', | ||||
|             'CMD sh -c "echo \'Deaf To All But The Song\'' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag='isolation', | ||||
|             isolation='default' | ||||
|         ) | ||||
| 
 | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|     @requires_api_version('1.23') | ||||
|     def test_build_labels(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM scratch', | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         labels = {'test': 'OK'} | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag='labels', labels=labels | ||||
|         ) | ||||
|         self.tmp_imgs.append('labels') | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         info = self.client.inspect_image('labels') | ||||
|         assert info['Config']['Labels'] == labels | ||||
| 
 | ||||
|     @requires_api_version('1.25') | ||||
|     def test_build_with_cache_from(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'ENV FOO=bar', | ||||
|             'RUN touch baz', | ||||
|             'RUN touch bax', | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build(fileobj=script, tag='build1') | ||||
|         self.tmp_imgs.append('build1') | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag='build2', cache_from=['build1'], | ||||
|             decode=True | ||||
|         ) | ||||
|         self.tmp_imgs.append('build2') | ||||
|         counter = 0 | ||||
|         for chunk in stream: | ||||
|             if 'Using cache' in chunk.get('stream', ''): | ||||
|                 counter += 1 | ||||
|         assert counter == 3 | ||||
|         self.client.remove_image('build2') | ||||
| 
 | ||||
|         counter = 0 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag='build2', cache_from=['nosuchtag'], | ||||
|             decode=True | ||||
|         ) | ||||
|         for chunk in stream: | ||||
|             if 'Using cache' in chunk.get('stream', ''): | ||||
|                 counter += 1 | ||||
|         assert counter == 0 | ||||
| 
 | ||||
|     @requires_api_version('1.29') | ||||
|     def test_build_container_with_target(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox as first', | ||||
|             'RUN mkdir -p /tmp/test', | ||||
|             'RUN touch /tmp/silence.tar.gz', | ||||
|             'FROM alpine:latest', | ||||
|             'WORKDIR /root/' | ||||
|             'COPY --from=first /tmp/silence.tar.gz .', | ||||
|             'ONBUILD RUN echo "This should not be in the final image"' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, target='first', tag='build1' | ||||
|         ) | ||||
|         self.tmp_imgs.append('build1') | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         info = self.client.inspect_image('build1') | ||||
|         assert not info['Config']['OnBuild'] | ||||
| 
 | ||||
|     @requires_api_version('1.25') | ||||
|     def test_build_with_network_mode(self): | ||||
|         # Set up pingable endpoint on custom network | ||||
|         network = self.client.create_network(random_name())['Id'] | ||||
|         self.tmp_networks.append(network) | ||||
|         container = self.client.create_container(TEST_IMG, 'top') | ||||
|         self.tmp_containers.append(container) | ||||
|         self.client.start(container) | ||||
|         self.client.connect_container_to_network( | ||||
|             container, network, aliases=['pingtarget.docker'] | ||||
|         ) | ||||
| 
 | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN ping -c1 pingtarget.docker' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, network_mode=network, | ||||
|             tag='dockerpytest_customnetbuild' | ||||
|         ) | ||||
| 
 | ||||
|         self.tmp_imgs.append('dockerpytest_customnetbuild') | ||||
|         for chunk in stream: | ||||
|             pass | ||||
| 
 | ||||
|         assert self.client.inspect_image('dockerpytest_customnetbuild') | ||||
| 
 | ||||
|         script.seek(0) | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, network_mode='none', | ||||
|             tag='dockerpytest_nonebuild', nocache=True, decode=True | ||||
|         ) | ||||
| 
 | ||||
|         self.tmp_imgs.append('dockerpytest_nonebuild') | ||||
|         logs = [chunk for chunk in stream] | ||||
|         assert 'errorDetail' in logs[-1] | ||||
|         assert logs[-1]['errorDetail']['code'] == 1 | ||||
| 
 | ||||
|         with pytest.raises(errors.NotFound): | ||||
|             self.client.inspect_image('dockerpytest_nonebuild') | ||||
| 
 | ||||
|     @requires_api_version('1.27') | ||||
|     def test_build_with_extra_hosts(self): | ||||
|         img_name = 'dockerpytest_extrahost_build' | ||||
|         self.tmp_imgs.append(img_name) | ||||
| 
 | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN ping -c1 hello.world.test', | ||||
|             'RUN ping -c1 extrahost.local.test', | ||||
|             'RUN cp /etc/hosts /hosts-file' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, tag=img_name, | ||||
|             extra_hosts={ | ||||
|                 'extrahost.local.test': '127.0.0.1', | ||||
|                 'hello.world.test': '127.0.0.1', | ||||
|             }, decode=True | ||||
|         ) | ||||
|         for chunk in stream: | ||||
|             if 'errorDetail' in chunk: | ||||
|                 pytest.fail(chunk) | ||||
| 
 | ||||
|         assert self.client.inspect_image(img_name) | ||||
|         ctnr = self.run_container(img_name, 'cat /hosts-file') | ||||
|         logs = self.client.logs(ctnr) | ||||
|         if six.PY3: | ||||
|             logs = logs.decode('utf-8') | ||||
|         assert '127.0.0.1\textrahost.local.test' in logs | ||||
|         assert '127.0.0.1\thello.world.test' in logs | ||||
| 
 | ||||
|     @requires_experimental(until=None) | ||||
|     @requires_api_version('1.25') | ||||
|     def test_build_squash(self): | ||||
|         script = io.BytesIO('\n'.join([ | ||||
|             'FROM busybox', | ||||
|             'RUN echo blah > /file_1', | ||||
|             'RUN echo blahblah > /file_2', | ||||
|             'RUN echo blahblahblah > /file_3' | ||||
|         ]).encode('ascii')) | ||||
| 
 | ||||
|         def build_squashed(squash): | ||||
|             tag = 'squash' if squash else 'nosquash' | ||||
|             stream = self.client.build( | ||||
|                 fileobj=script, tag=tag, squash=squash | ||||
|             ) | ||||
|             self.tmp_imgs.append(tag) | ||||
|             for chunk in stream: | ||||
|                 pass | ||||
| 
 | ||||
|             return self.client.inspect_image(tag) | ||||
| 
 | ||||
|         non_squashed = build_squashed(False) | ||||
|         squashed = build_squashed(True) | ||||
|         assert len(non_squashed['RootFS']['Layers']) == 4 | ||||
|         assert len(squashed['RootFS']['Layers']) == 2 | ||||
| 
 | ||||
|     def test_build_stderr_data(self): | ||||
|         control_chars = ['\x1b[91m', '\x1b[0m'] | ||||
|         snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)' | ||||
|         script = io.BytesIO(b'\n'.join([ | ||||
|             b'FROM busybox', | ||||
|             'RUN sh -c ">&2 echo \'{0}\'"'.format(snippet).encode('utf-8') | ||||
|         ])) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             fileobj=script, decode=True, nocache=True | ||||
|         ) | ||||
|         lines = [] | ||||
|         for chunk in stream: | ||||
|             lines.append(chunk.get('stream')) | ||||
|         expected = '{0}{2}\n{1}'.format( | ||||
|             control_chars[0], control_chars[1], snippet | ||||
|         ) | ||||
|         assert any([line == expected for line in lines]) | ||||
| 
 | ||||
|     def test_build_gzip_encoding(self): | ||||
|         base_dir = tempfile.mkdtemp() | ||||
|         self.addCleanup(shutil.rmtree, base_dir) | ||||
| 
 | ||||
|         with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: | ||||
|             f.write("\n".join([ | ||||
|                 'FROM busybox', | ||||
|                 'ADD . /test', | ||||
|             ])) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             path=base_dir, decode=True, nocache=True, | ||||
|             gzip=True | ||||
|         ) | ||||
| 
 | ||||
|         lines = [] | ||||
|         for chunk in stream: | ||||
|             lines.append(chunk) | ||||
| 
 | ||||
|         assert 'Successfully built' in lines[-1]['stream'] | ||||
| 
 | ||||
|     def test_build_with_dockerfile_empty_lines(self): | ||||
|         base_dir = tempfile.mkdtemp() | ||||
|         self.addCleanup(shutil.rmtree, base_dir) | ||||
|         with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: | ||||
|             f.write('FROM busybox\n') | ||||
|         with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: | ||||
|             f.write('\n'.join([ | ||||
|                 '   ', | ||||
|                 '', | ||||
|                 '\t\t', | ||||
|                 '\t     ', | ||||
|             ])) | ||||
| 
 | ||||
|         stream = self.client.build( | ||||
|             path=base_dir, decode=True, nocache=True | ||||
|         ) | ||||
| 
 | ||||
|         lines = [] | ||||
|         for chunk in stream: | ||||
|             lines.append(chunk) | ||||
|         assert 'Successfully built' in lines[-1]['stream'] | ||||
| 
 | ||||
|     def test_build_gzip_custom_encoding(self): | ||||
|         with pytest.raises(errors.DockerException): | ||||
|             self.client.build(path='.', gzip=True, encoding='text/html') | ||||
| 
 | ||||
|     @requires_api_version('1.32') | ||||
|     @requires_experimental(until=None) | ||||
|     def test_build_invalid_platform(self): | ||||
|         script = io.BytesIO('FROM busybox\n'.encode('ascii')) | ||||
| 
 | ||||
|         with pytest.raises(errors.APIError) as excinfo: | ||||
|             stream = self.client.build(fileobj=script, platform='foobar') | ||||
|             for _ in stream: | ||||
|                 pass | ||||
| 
 | ||||
|         # Some API versions incorrectly returns 500 status; assert 4xx or 5xx | ||||
|         assert excinfo.value.is_error() | ||||
|         assert 'unknown operating system' in excinfo.exconly() \ | ||||
|                or 'invalid platform' in excinfo.exconly() | ||||
| 
 | ||||
|     def test_build_out_of_context_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') | ||||
|         with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: | ||||
|             f.write('.dockerignore\n') | ||||
|         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, | ||||
|             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) == 3 | ||||
|         assert sorted([b'.', b'..', b'file.txt']) == sorted(lsdata) | ||||
| 
 | ||||
|     def test_build_in_context_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') | ||||
|         with open(os.path.join(base_dir, '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='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'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) | ||||
|         abs_dockerfile_path = os.path.join(base_dir, 'custom.dockerfile') | ||||
|         with open(os.path.join(base_dir, 'file.txt'), 'w') as f: | ||||
|             f.write('hello world') | ||||
|         with open(abs_dockerfile_path, '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=abs_dockerfile_path, 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'custom.dockerfile'] | ||||
|         ) == sorted(lsdata) | ||||
| 
 | ||||
|     @requires_api_version('1.31') | ||||
|     @pytest.mark.xfail( | ||||
|         True, | ||||
|         reason='Currently fails on 18.09: ' | ||||
|                'https://github.com/moby/moby/issues/37920' | ||||
|     ) | ||||
|     def test_prune_builds(self): | ||||
|         prune_result = self.client.prune_builds() | ||||
|         assert 'SpaceReclaimed' in prune_result | ||||
|         assert isinstance(prune_result['SpaceReclaimed'], int) | ||||
|  | @ -0,0 +1,130 @@ | |||
| import os | ||||
| import shutil | ||||
| import unittest | ||||
| 
 | ||||
| import docker | ||||
| from .. import helpers | ||||
| from docker.utils import kwargs_from_env | ||||
| 
 | ||||
| TEST_IMG = 'alpine:3.10' | ||||
| TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION') | ||||
| 
 | ||||
| 
 | ||||
| class BaseIntegrationTest(unittest.TestCase): | ||||
|     """ | ||||
|     A base class for integration test cases. It cleans up the Docker server | ||||
|     after itself. | ||||
|     """ | ||||
| 
 | ||||
|     def setUp(self): | ||||
|         self.tmp_imgs = [] | ||||
|         self.tmp_containers = [] | ||||
|         self.tmp_folders = [] | ||||
|         self.tmp_volumes = [] | ||||
|         self.tmp_networks = [] | ||||
|         self.tmp_plugins = [] | ||||
|         self.tmp_secrets = [] | ||||
|         self.tmp_configs = [] | ||||
| 
 | ||||
|     def tearDown(self): | ||||
|         client = docker.from_env(version=TEST_API_VERSION, use_ssh_client=True) | ||||
|         try: | ||||
|             for img in self.tmp_imgs: | ||||
|                 try: | ||||
|                     client.api.remove_image(img) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
|             for container in self.tmp_containers: | ||||
|                 try: | ||||
|                     client.api.remove_container(container, force=True, v=True) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
|             for network in self.tmp_networks: | ||||
|                 try: | ||||
|                     client.api.remove_network(network) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
|             for volume in self.tmp_volumes: | ||||
|                 try: | ||||
|                     client.api.remove_volume(volume) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
| 
 | ||||
|             for secret in self.tmp_secrets: | ||||
|                 try: | ||||
|                     client.api.remove_secret(secret) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
| 
 | ||||
|             for config in self.tmp_configs: | ||||
|                 try: | ||||
|                     client.api.remove_config(config) | ||||
|                 except docker.errors.APIError: | ||||
|                     pass | ||||
| 
 | ||||
|             for folder in self.tmp_folders: | ||||
|                 shutil.rmtree(folder) | ||||
|         finally: | ||||
|             client.close() | ||||
| 
 | ||||
| 
 | ||||
| class BaseAPIIntegrationTest(BaseIntegrationTest): | ||||
|     """ | ||||
|     A test case for `APIClient` integration tests. It sets up an `APIClient` | ||||
|     as `self.client`. | ||||
|     """ | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         cls.client = cls.get_client_instance() | ||||
|         cls.client.pull(TEST_IMG) | ||||
| 
 | ||||
|     def tearDown(self): | ||||
|         super(BaseAPIIntegrationTest, self).tearDown() | ||||
|         self.client.close() | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def get_client_instance(): | ||||
|         return docker.APIClient( | ||||
|             version=TEST_API_VERSION, | ||||
|             timeout=60, | ||||
|             use_ssh_client=True, | ||||
|             **kwargs_from_env() | ||||
|         ) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _init_swarm(client, **kwargs): | ||||
|         return client.init_swarm( | ||||
|             '127.0.0.1', listen_addr=helpers.swarm_listen_addr(), **kwargs | ||||
|         ) | ||||
| 
 | ||||
|     def run_container(self, *args, **kwargs): | ||||
|         container = self.client.create_container(*args, **kwargs) | ||||
|         self.tmp_containers.append(container) | ||||
|         self.client.start(container) | ||||
|         exitcode = self.client.wait(container)['StatusCode'] | ||||
| 
 | ||||
|         if exitcode != 0: | ||||
|             output = self.client.logs(container) | ||||
|             raise Exception( | ||||
|                 "Container exited with code {}:\n{}" | ||||
|                 .format(exitcode, output)) | ||||
| 
 | ||||
|         return container | ||||
| 
 | ||||
|     def create_and_start(self, image=TEST_IMG, command='top', **kwargs): | ||||
|         container = self.client.create_container( | ||||
|             image=image, command=command, **kwargs) | ||||
|         self.tmp_containers.append(container) | ||||
|         self.client.start(container) | ||||
|         return container | ||||
| 
 | ||||
|     def execute(self, container, cmd, exit_code=0, **kwargs): | ||||
|         exc = self.client.exec_create(container, cmd, **kwargs) | ||||
|         output = self.client.exec_start(exc) | ||||
|         actual_exit_code = self.client.exec_inspect(exc)['ExitCode'] | ||||
|         msg = "Expected `{}` to exit with code {} but returned {}:\n{}".format( | ||||
|             " ".join(cmd), exit_code, actual_exit_code, output) | ||||
|         assert actual_exit_code == exit_code, msg | ||||
| 
 | ||||
|     def init_swarm(self, **kwargs): | ||||
|         return self._init_swarm(self.client, **kwargs) | ||||
		Loading…
	
		Reference in New Issue