Merge pull request #2476 from feliperuhland/add-search-images-limit

Add limit parameter to image search endpoint
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f119c9..b692508 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@
         strategy:
             max-parallel: 1
             matrix:
-                python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+                python-version: [3.6, 3.7, 3.8, 3.9]
 
         steps:
             - uses: actions/checkout@v2
@@ -18,8 +18,8 @@
                   python-version: ${{ matrix.python-version }}
             - name: Install dependencies
               run: |
-                  python -m pip install --upgrade pip
-                  pip install -r test-requirements.txt -r requirements.txt
+                  python3 -m pip install --upgrade pip
+                  pip3 install -r test-requirements.txt -r requirements.txt
             - name: Test with pytest
               run: |
                   docker logout
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 7679f80..32113fe 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -4,7 +4,7 @@
   configuration: docs/conf.py
 
 python:
-  version: 3.5
+  version: 3.6
   install:
     - requirements: docs-requirements.txt
     - requirements: requirements.txt
diff --git a/Dockerfile b/Dockerfile
index 7309a83..22732de 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,7 @@
-ARG PYTHON_VERSION=2.7
+ARG PYTHON_VERSION=3.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
 
diff --git a/Dockerfile-py3 b/Dockerfile-py3
deleted file mode 100644
index 22732de..0000000
--- a/Dockerfile-py3
+++ /dev/null
@@ -1,15 +0,0 @@
-ARG PYTHON_VERSION=3.7
-
-FROM python:${PYTHON_VERSION}
-
-RUN mkdir /src
-WORKDIR /src
-
-COPY requirements.txt /src/requirements.txt
-RUN pip install -r requirements.txt
-
-COPY test-requirements.txt /src/test-requirements.txt
-RUN pip install -r test-requirements.txt
-
-COPY . /src
-RUN pip install .
diff --git a/Jenkinsfile b/Jenkinsfile
index 471072b..f524ae7 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,7 +1,6 @@
 #!groovy
 
 def imageNameBase = "dockerpinata/docker-py"
-def imageNamePy2
 def imageNamePy3
 def imageDindSSH
 def images = [:]
@@ -22,12 +21,10 @@
     stage("build image") {
       checkout(scm)
 
-      imageNamePy2 = "${imageNameBase}:py2-${gitCommit()}"
       imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}"
       imageDindSSH = "${imageNameBase}:sshdind-${gitCommit()}"
       withDockerRegistry(credentialsId:'dockerbuildbot-index.docker.io') {
         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")
       }
     }
@@ -73,7 +70,7 @@
     throw new Exception("Need Docker version to test, e.g.: `runTests(dockerVersion: '19.03.12')`")
   }
   if (!pythonVersion) {
-    throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py2.7')`")
+    throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py3.7')`")
   }
 
   { ->
diff --git a/Makefile b/Makefile
index 70d7083..60d9984 100644
--- a/Makefile
+++ b/Makefile
@@ -6,13 +6,9 @@
 
 .PHONY: clean
 clean:
-	-docker rm -f dpy-dind-py2 dpy-dind-py3 dpy-dind-certs dpy-dind-ssl
+	-docker rm -f dpy-dind-py3 dpy-dind-certs dpy-dind-ssl
 	find -name "__pycache__" | xargs rm -rf
 
-.PHONY: build
-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 .
@@ -30,20 +26,12 @@
 	docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
 
 .PHONY: test
-test: flake8 unit-test unit-test-py3 integration-dind integration-dind-ssl
-
-.PHONY: unit-test
-unit-test: build
-	docker run -t --rm docker-sdk-python py.test tests/unit
+test: flake8 unit-test-py3 integration-dind integration-dind-ssl
 
 .PHONY: unit-test-py3
 unit-test-py3: build-py3
 	docker run -t --rm docker-sdk-python3 py.test tests/unit
 
-.PHONY: integration-test
-integration-test: build
-	docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python py.test -v tests/integration/${file}
-
 .PHONY: integration-test-py3
 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}
@@ -53,16 +41,7 @@
 	docker network inspect dpy-tests || docker network create dpy-tests
 
 .PHONY: integration-dind
-integration-dind: integration-dind-py2 integration-dind-py3
-
-.PHONY: integration-dind-py2
-integration-dind-py2: build setup-network
-	docker rm -vf dpy-dind-py2 || :
-	docker run -d --network dpy-tests --name dpy-dind-py2 --privileged\
-		docker:${TEST_ENGINE_VERSION}-dind dockerd -H tcp://0.0.0.0:2375 --experimental
-	docker run -t --rm --env="DOCKER_HOST=tcp://dpy-dind-py2:2375" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
-		--network dpy-tests docker-sdk-python py.test tests/integration/${file}
-	docker rm -vf dpy-dind-py2
+integration-dind: integration-dind-py3
 
 .PHONY: integration-dind-py3
 integration-dind-py3: build-py3 setup-network
@@ -73,16 +52,6 @@
 		--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
@@ -97,7 +66,7 @@
 
 
 .PHONY: integration-dind-ssl
-integration-dind-ssl: build-dind-certs build build-py3
+integration-dind-ssl: build-dind-certs build-py3
 	docker rm -vf dpy-dind-certs dpy-dind-ssl || :
 	docker run -d --name dpy-dind-certs dpy-dind-certs
 	docker run -d --env="DOCKER_HOST=tcp://localhost:2375" --env="DOCKER_TLS_VERIFY=1"\
@@ -108,20 +77,17 @@
 		--tlskey=/certs/server-key.pem -H tcp://0.0.0.0:2375 --experimental
 	docker run -t --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\
 		--env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
-		--network dpy-tests docker-sdk-python py.test tests/integration/${file}
-	docker run -t --rm --volumes-from dpy-dind-ssl --env="DOCKER_HOST=tcp://docker:2375"\
-		--env="DOCKER_TLS_VERIFY=1" --env="DOCKER_CERT_PATH=/certs" --env="DOCKER_TEST_API_VERSION=${TEST_API_VERSION}"\
 		--network dpy-tests docker-sdk-python3 py.test tests/integration/${file}
 	docker rm -vf dpy-dind-ssl dpy-dind-certs
 
 .PHONY: flake8
-flake8: build
-	docker run -t --rm docker-sdk-python flake8 docker tests
+flake8: build-py3
+	docker run -t --rm docker-sdk-python3 flake8 docker tests
 
 .PHONY: docs
 docs: build-docs
 	docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build
 
 .PHONY: shell
-shell: build
-	docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python python
+shell: build-py3
+	docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 python
diff --git a/README.md b/README.md
index 8ce684b..4fc31f7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Docker SDK for Python
 
-[![Build Status](https://travis-ci.org/docker/docker-py.svg?branch=master)](https://travis-ci.org/docker/docker-py)
+[![Build Status](https://github.com/docker/docker-py/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/docker/docker-py/actions/workflows/ci.yml/)
 
 A Python library for the Docker Engine API. It lets you do anything the `docker` command does, but from within Python apps – run containers, manage containers, manage Swarms, etc.
 
diff --git a/docker/api/client.py b/docker/api/client.py
index 2b67291..ee9ad9c 100644
--- a/docker/api/client.py
+++ b/docker/api/client.py
@@ -1,10 +1,10 @@
 import json
 import struct
+import urllib
 from functools import partial
 
 import requests
 import requests.exceptions
-import six
 import websocket
 
 from .. import auth
@@ -192,12 +192,12 @@
         # version detection needs to be after unix adapter mounting
         if version is None or (isinstance(
                                 version,
-                                six.string_types
+                                str
                                 ) and version.lower() == 'auto'):
             self._version = self._retrieve_server_version()
         else:
             self._version = version
-        if not isinstance(self._version, six.string_types):
+        if not isinstance(self._version, str):
             raise DockerException(
                 'Version parameter must be a string or None. Found {0}'.format(
                     type(version).__name__
@@ -246,13 +246,13 @@
 
     def _url(self, pathfmt, *args, **kwargs):
         for arg in args:
-            if not isinstance(arg, six.string_types):
+            if not isinstance(arg, str):
                 raise ValueError(
                     'Expected a string but found {0} ({1}) '
                     'instead'.format(arg, type(arg))
                 )
 
-        quote_f = partial(six.moves.urllib.parse.quote, safe="/:")
+        quote_f = partial(urllib.parse.quote, safe="/:")
         args = map(quote_f, args)
 
         if kwargs.get('versioned_api', True):
@@ -284,7 +284,7 @@
         # so we do this disgusting thing here.
         data2 = {}
         if data is not None and isinstance(data, dict):
-            for k, v in six.iteritems(data):
+            for k, v in iter(data.items()):
                 if v is not None:
                     data2[k] = v
         elif data is not None:
@@ -320,12 +320,10 @@
             sock = response.raw._fp.fp.raw.sock
         elif self.base_url.startswith('http+docker://ssh'):
             sock = response.raw._fp.fp.channel
-        elif six.PY3:
+        else:
             sock = response.raw._fp.fp.raw
             if self.base_url.startswith("https://"):
                 sock = sock._sock
-        else:
-            sock = response.raw._fp.fp._sock
         try:
             # Keep a reference to the response to stop it being garbage
             # collected. If the response is garbage collected, it will
@@ -465,7 +463,7 @@
                 self._result(res, binary=True)
 
         self._raise_for_status(res)
-        sep = six.binary_type()
+        sep = b''
         if stream:
             return self._multiplexed_response_stream_helper(res)
         else:
diff --git a/docker/api/container.py b/docker/api/container.py
index 754b5dc..369eba9 100644
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -244,9 +244,9 @@
 
         .. code-block:: python
 
-            container_id = cli.create_container(
+            container_id = client.api.create_container(
                 'busybox', 'ls', ports=[1111, 2222],
-                host_config=cli.create_host_config(port_bindings={
+                host_config=client.api.create_host_config(port_bindings={
                     1111: 4567,
                     2222: None
                 })
@@ -258,22 +258,22 @@
 
         .. code-block:: python
 
-            cli.create_host_config(port_bindings={1111: ('127.0.0.1', 4567)})
+            client.api.create_host_config(port_bindings={1111: ('127.0.0.1', 4567)})
 
         Or without host port assignment:
 
         .. code-block:: python
 
-            cli.create_host_config(port_bindings={1111: ('127.0.0.1',)})
+            client.api.create_host_config(port_bindings={1111: ('127.0.0.1',)})
 
         If you wish to use UDP instead of TCP (default), you need to declare
         ports as such in both the config and host config:
 
         .. code-block:: python
 
-            container_id = cli.create_container(
+            container_id = client.api.create_container(
                 'busybox', 'ls', ports=[(1111, 'udp'), 2222],
-                host_config=cli.create_host_config(port_bindings={
+                host_config=client.api.create_host_config(port_bindings={
                     '1111/udp': 4567, 2222: None
                 })
             )
@@ -283,7 +283,7 @@
 
         .. code-block:: python
 
-            cli.create_host_config(port_bindings={
+            client.api.create_host_config(port_bindings={
                 1111: [1234, 4567]
             })
 
@@ -291,7 +291,7 @@
 
         .. code-block:: python
 
-            cli.create_host_config(port_bindings={
+            client.api.create_host_config(port_bindings={
                 1111: [
                     ('192.168.0.100', 1234),
                     ('192.168.0.101', 1234)
@@ -307,9 +307,9 @@
 
         .. code-block:: python
 
-            container_id = cli.create_container(
+            container_id = client.api.create_container(
                 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
-                host_config=cli.create_host_config(binds={
+                host_config=client.api.create_host_config(binds={
                     '/home/user1/': {
                         'bind': '/mnt/vol2',
                         'mode': 'rw',
@@ -326,9 +326,9 @@
 
         .. code-block:: python
 
-            container_id = cli.create_container(
+            container_id = client.api.create_container(
                 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'],
-                host_config=cli.create_host_config(binds=[
+                host_config=client.api.create_host_config(binds=[
                     '/home/user1/:/mnt/vol2',
                     '/var/www:/mnt/vol1:ro',
                 ])
@@ -346,15 +346,15 @@
 
         .. code-block:: python
 
-            networking_config = docker_client.create_networking_config({
-                'network1': docker_client.create_endpoint_config(
+            networking_config = client.api.create_networking_config({
+                'network1': client.api.create_endpoint_config(
                     ipv4_address='172.28.0.124',
                     aliases=['foo', 'bar'],
                     links=['container2']
                 )
             })
 
-            ctnr = docker_client.create_container(
+            ctnr = client.api.create_container(
                 img, command, networking_config=networking_config
             )
 
@@ -581,7 +581,7 @@
 
         Example:
 
-            >>> cli.create_host_config(privileged=True, cap_drop=['MKNOD'],
+            >>> client.api.create_host_config(privileged=True, cap_drop=['MKNOD'],
                                        volumes_from=['nostalgic_newton'])
             {'CapDrop': ['MKNOD'], 'LxcConf': None, 'Privileged': True,
              'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
@@ -612,11 +612,11 @@
 
         Example:
 
-            >>> docker_client.create_network('network1')
-            >>> networking_config = docker_client.create_networking_config({
-                'network1': docker_client.create_endpoint_config()
+            >>> client.api.create_network('network1')
+            >>> networking_config = client.api.create_networking_config({
+                'network1': client.api.create_endpoint_config()
             })
-            >>> container = docker_client.create_container(
+            >>> container = client.api.create_container(
                 img, command, networking_config=networking_config
             )
 
@@ -650,7 +650,7 @@
 
         Example:
 
-            >>> endpoint_config = client.create_endpoint_config(
+            >>> endpoint_config = client.api.create_endpoint_config(
                 aliases=['web', 'app'],
                 links={'app_db': 'db', 'another': None},
                 ipv4_address='132.65.0.123'
@@ -729,7 +729,7 @@
 
             >>> c = docker.APIClient()
             >>> f = open('./sh_bin.tar', 'wb')
-            >>> bits, stat = c.get_archive(container, '/bin/sh')
+            >>> bits, stat = c.api.get_archive(container, '/bin/sh')
             >>> print(stat)
             {'name': 'sh', 'size': 1075464, 'mode': 493,
              'mtime': '2018-10-01T15:37:48-07:00', 'linkTarget': ''}
@@ -916,7 +916,7 @@
 
             .. code-block:: python
 
-                >>> cli.port('7174d6347063', 80)
+                >>> client.api.port('7174d6347063', 80)
                 [{'HostIp': '0.0.0.0', 'HostPort': '80'}]
         """
         res = self._get(self._url("/containers/{0}/json", container))
@@ -1095,10 +1095,10 @@
 
         Example:
 
-            >>> container = cli.create_container(
+            >>> container = client.api.create_container(
             ...     image='busybox:latest',
             ...     command='/bin/sleep 30')
-            >>> cli.start(container=container.get('Id'))
+            >>> client.api.start(container=container.get('Id'))
         """
         if args or kwargs:
             raise errors.DeprecatedMethod(
diff --git a/docker/api/image.py b/docker/api/image.py
index 4658cde..772101f 100644
--- a/docker/api/image.py
+++ b/docker/api/image.py
@@ -31,7 +31,7 @@
 
         Example:
 
-            >>> image = cli.get_image("busybox:latest")
+            >>> image = client.api.get_image("busybox:latest")
             >>> f = open('/tmp/busybox-latest.tar', 'wb')
             >>> for chunk in image:
             >>>   f.write(chunk)
@@ -379,7 +379,7 @@
 
         Example:
 
-            >>> for line in cli.pull('busybox', stream=True, decode=True):
+            >>> for line in client.api.pull('busybox', stream=True, decode=True):
             ...     print(json.dumps(line, indent=4))
             {
                 "status": "Pulling image (latest) from busybox",
@@ -458,7 +458,7 @@
                 If the server returns an error.
 
         Example:
-            >>> for line in cli.push('yourname/app', stream=True, decode=True):
+            >>> for line in client.api.push('yourname/app', stream=True, decode=True):
             ...   print(line)
             {'status': 'Pushing repository yourname/app (1 tags)'}
             {'status': 'Pushing','progressDetail': {}, 'id': '511136ea3c5a'}
@@ -554,7 +554,7 @@
 
         Example:
 
-            >>> client.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
+            >>> client.api.tag('ubuntu', 'localhost:5000/ubuntu', 'latest',
                            force=True)
         """
         params = {
diff --git a/docker/context/context.py b/docker/context/context.py
index 2413b2e..f4aff6b 100644
--- a/docker/context/context.py
+++ b/docker/context/context.py
@@ -11,6 +11,7 @@
 
 class Context:
     """A context."""
+
     def __init__(self, name, orchestrator=None, host=None, endpoints=None,
                  tls=False):
         if not name:
@@ -127,8 +128,12 @@
                 elif filename.startswith("key"):
                     key = os.path.join(tls_dir, endpoint, filename)
             if all([ca_cert, cert, key]):
+                verify = None
+                if endpoint == "docker" and not self.endpoints["docker"].get(
+                        "SkipTLSVerify", False):
+                    verify = True
                 certs[endpoint] = TLSConfig(
-                    client_cert=(cert, key), ca_cert=ca_cert)
+                    client_cert=(cert, key), ca_cert=ca_cert, verify=verify)
         self.tls_cfg = certs
         self.tls_path = tls_dir
 
diff --git a/docker/models/secrets.py b/docker/models/secrets.py
index ca11ede..e2ee88a 100644
--- a/docker/models/secrets.py
+++ b/docker/models/secrets.py
@@ -30,6 +30,7 @@
 
     def create(self, **kwargs):
         obj = self.client.api.create_secret(**kwargs)
+        obj.setdefault("Spec", {})["Name"] = kwargs.get("name")
         return self.prepare_model(obj)
     create.__doc__ = APIClient.create_secret.__doc__
 
diff --git a/docker/transport/sshconn.py b/docker/transport/sshconn.py
index a761ef5..fb5c6bb 100644
--- a/docker/transport/sshconn.py
+++ b/docker/transport/sshconn.py
@@ -53,7 +53,7 @@
                 signal.signal(signal.SIGINT, signal.SIG_IGN)
             preexec_func = f
 
-        env = dict(os.environ) 
+        env = dict(os.environ)
 
         # drop LD_LIBRARY_PATH and SSL_CERT_FILE
         env.pop('LD_LIBRARY_PATH', None)
@@ -65,7 +65,7 @@
             shell=True,
             stdout=subprocess.PIPE,
             stdin=subprocess.PIPE,
-            preexec_fn=preexec_func)
+            preexec_fn=None if constants.IS_WINDOWS_PLATFORM else preexec_func)
 
     def _write(self, data):
         if not self.proc or self.proc.stdin.closed:
diff --git a/docker/utils/ports.py b/docker/utils/ports.py
index a50cc02..10b19d7 100644
--- a/docker/utils/ports.py
+++ b/docker/utils/ports.py
@@ -3,7 +3,7 @@
 PORT_SPEC = re.compile(
     "^"  # Match full string
     "("  # External part
-    r"((?P<host>[a-fA-F\d.:]+):)?"  # Address
+    r"(\[?(?P<host>[a-fA-F\d.:]+)\]?:)?"  # Address
     r"(?P<ext>[\d]*)(-(?P<ext_end>[\d]+))?:"  # External range
     ")?"
     r"(?P<int>[\d]+)(-(?P<int_end>[\d]+))?"  # Internal range
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index 1b195e2..f703cbd 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -7,8 +7,6 @@
 from datetime import datetime
 from distutils.version import StrictVersion
 
-import six
-
 from .. import errors
 from .. import tls
 from ..constants import DEFAULT_HTTP_HOST
@@ -16,11 +14,7 @@
 from ..constants import DEFAULT_NPIPE
 from ..constants import BYTE_UNITS
 
-if six.PY2:
-    from urllib import splitnport
-    from urlparse import urlparse
-else:
-    from urllib.parse import splitnport, urlparse
+from urllib.parse import splitnport, urlparse
 
 
 def create_ipam_pool(*args, **kwargs):
@@ -39,8 +33,7 @@
 
 def decode_json_header(header):
     data = base64.b64decode(header)
-    if six.PY3:
-        data = data.decode('utf-8')
+    data = data.decode('utf-8')
     return json.loads(data)
 
 
@@ -80,7 +73,7 @@
         if len(binding) == 2:
             result['HostPort'] = binding[1]
             result['HostIp'] = binding[0]
-        elif isinstance(binding[0], six.string_types):
+        elif isinstance(binding[0], str):
             result['HostIp'] = binding[0]
         else:
             result['HostPort'] = binding[0]
@@ -104,7 +97,7 @@
 
 def convert_port_bindings(port_bindings):
     result = {}
-    for k, v in six.iteritems(port_bindings):
+    for k, v in iter(port_bindings.items()):
         key = str(k)
         if '/' not in key:
             key += '/tcp'
@@ -121,7 +114,7 @@
 
     result = []
     for k, v in binds.items():
-        if isinstance(k, six.binary_type):
+        if isinstance(k, bytes):
             k = k.decode('utf-8')
 
         if isinstance(v, dict):
@@ -132,7 +125,7 @@
                 )
 
             bind = v['bind']
-            if isinstance(bind, six.binary_type):
+            if isinstance(bind, bytes):
                 bind = bind.decode('utf-8')
 
             if 'ro' in v:
@@ -143,13 +136,13 @@
                 mode = 'rw'
 
             result.append(
-                six.text_type('{0}:{1}:{2}').format(k, bind, mode)
+                str('{0}:{1}:{2}').format(k, bind, mode)
             )
         else:
-            if isinstance(v, six.binary_type):
+            if isinstance(v, bytes):
                 v = v.decode('utf-8')
             result.append(
-                six.text_type('{0}:{1}:rw').format(k, v)
+                str('{0}:{1}:rw').format(k, v)
             )
     return result
 
@@ -166,7 +159,7 @@
 
     result = {}
     for mount in tmpfs:
-        if isinstance(mount, six.string_types):
+        if isinstance(mount, str):
             if ":" in mount:
                 name, options = mount.split(":", 1)
             else:
@@ -191,7 +184,7 @@
 
     result = []
     for n in networks:
-        if isinstance(n, six.string_types):
+        if isinstance(n, str):
             n = {'Target': n}
         result.append(n)
     return result
@@ -302,7 +295,7 @@
         if isinstance(device, dict):
             device_list.append(device)
             continue
-        if not isinstance(device, six.string_types):
+        if not isinstance(device, str):
             raise errors.DockerException(
                 'Invalid device type {0}'.format(type(device))
             )
@@ -372,13 +365,13 @@
 
 def convert_filters(filters):
     result = {}
-    for k, v in six.iteritems(filters):
+    for k, v in iter(filters.items()):
         if isinstance(v, bool):
             v = 'true' if v else 'false'
         if not isinstance(v, list):
             v = [v, ]
         result[k] = [
-            str(item) if not isinstance(item, six.string_types) else item
+            str(item) if not isinstance(item, str) else item
             for item in v
         ]
     return json.dumps(result)
@@ -391,7 +384,7 @@
 
 
 def parse_bytes(s):
-    if isinstance(s, six.integer_types + (float,)):
+    if isinstance(s, (int, float,)):
         return s
     if len(s) == 0:
         return 0
@@ -433,7 +426,7 @@
 
 def normalize_links(links):
     if isinstance(links, dict):
-        links = six.iteritems(links)
+        links = iter(links.items())
 
     return ['{0}:{1}'.format(k, v) if v else k for k, v in sorted(links)]
 
@@ -468,8 +461,6 @@
 
 
 def split_command(command):
-    if six.PY2 and not isinstance(command, six.binary_type):
-        command = command.encode('utf-8')
     return shlex.split(command)
 
 
@@ -477,22 +468,22 @@
     def format_env(key, value):
         if value is None:
             return key
-        if isinstance(value, six.binary_type):
+        if isinstance(value, bytes):
             value = value.decode('utf-8')
 
         return u'{key}={value}'.format(key=key, value=value)
-    return [format_env(*var) for var in six.iteritems(environment)]
+    return [format_env(*var) for var in iter(environment.items())]
 
 
 def format_extra_hosts(extra_hosts, task=False):
     # Use format dictated by Swarm API if container is part of a task
     if task:
         return [
-            '{} {}'.format(v, k) for k, v in sorted(six.iteritems(extra_hosts))
+            '{} {}'.format(v, k) for k, v in sorted(iter(extra_hosts.items()))
         ]
 
     return [
-        '{}:{}'.format(k, v) for k, v in sorted(six.iteritems(extra_hosts))
+        '{}:{}'.format(k, v) for k, v in sorted(iter(extra_hosts.items()))
     ]
 
 
diff --git a/docs/change-log.md b/docs/change-log.md
index 546e071..8db3fc5 100644
--- a/docs/change-log.md
+++ b/docs/change-log.md
@@ -1,6 +1,14 @@
 Change log
 ==========
 
+4.4.4
+-----
+
+[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/73?closed=1)
+
+### Bugfixes
+- Remove `LD_LIBRARY_PATH` and `SSL_CERT_FILE` environment variables when shelling out to the ssh client
+
 4.4.3
 -----
 
diff --git a/requirements.txt b/requirements.txt
index 43a688f..1d0be30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@
 asn1crypto==0.22.0
 backports.ssl-match-hostname==3.5.0.1
 cffi==1.14.4
-cryptography==3.2
+cryptography==3.4.7
 enum34==1.1.6
 idna==2.5
 ipaddress==1.0.18
@@ -13,6 +13,5 @@
 pyparsing==2.2.0
 pywin32==227; sys_platform == 'win32'
 requests==2.20.0
-six==1.10.0
 urllib3==1.24.3
 websocket-client==0.56.0
diff --git a/setup.py b/setup.py
index 330ab3e..b692eab 100644
--- a/setup.py
+++ b/setup.py
@@ -11,18 +11,11 @@
 SOURCE_DIR = os.path.join(ROOT_DIR)
 
 requirements = [
-    'six >= 1.4.0',
     'websocket-client >= 0.32.0',
     'requests >= 2.14.2, != 2.18.0',
 ]
 
 extras_require = {
-    ':python_version < "3.5"': 'backports.ssl_match_hostname >= 3.5',
-    # While not imported explicitly, the ipaddress module is required for
-    # ssl_match_hostname to verify hosts match with certificates via
-    # ServerAltname: https://pypi.python.org/pypi/backports.ssl_match_hostname
-    ':python_version < "3.3"': 'ipaddress >= 1.0.16',
-
     # win32 APIs if on Windows (required for npipe support)
     ':sys_platform == "win32"': 'pywin32==227',
 
@@ -34,7 +27,7 @@
     # https://github.com/pypa/pip/issues/4391).  Once that's fixed, instead of
     # installing the extra dependencies, install the following instead:
     # 'requests[security] >= 2.5.2, != 2.11.0, != 2.12.2'
-    'tls': ['pyOpenSSL>=17.5.0', 'cryptography>=1.3.4', 'idna>=2.0.0'],
+    'tls': ['pyOpenSSL>=17.5.0', 'cryptography>=3.4.7', 'idna>=2.0.0'],
 
     # Only required when connecting using the ssh:// protocol
     'ssh': ['paramiko>=2.4.2'],
@@ -69,7 +62,7 @@
     install_requires=requirements,
     tests_require=test_requirements,
     extras_require=extras_require,
-    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
+    python_requires='>=3.6',
     zip_safe=False,
     test_suite='tests',
     classifiers=[
@@ -78,10 +71,7 @@
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
diff --git a/test-requirements.txt b/test-requirements.txt
index 24078e2..40161bb 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,4 @@
-setuptools==44.0.0 # last version with python 2.7 support
+setuptools==54.1.1
 coverage==4.5.2
 flake8==3.6.0
 mock==1.0.1
diff --git a/tests/Dockerfile-dind-certs b/tests/Dockerfile-dind-certs
index 2ab87ef..8829ff7 100644
--- a/tests/Dockerfile-dind-certs
+++ b/tests/Dockerfile-dind-certs
@@ -1,4 +1,4 @@
-ARG PYTHON_VERSION=2.7
+ARG PYTHON_VERSION=3.6
 
 FROM python:${PYTHON_VERSION}
 RUN mkdir /tmp/certs
diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py
index 65e611b..3087045 100644
--- a/tests/integration/api_container_test.py
+++ b/tests/integration/api_container_test.py
@@ -7,7 +7,6 @@
 
 import pytest
 import requests
-import six
 
 import docker
 from .. import helpers
@@ -35,7 +34,7 @@
         assert len(retrieved) == 1
         retrieved = retrieved[0]
         assert 'Command' in retrieved
-        assert retrieved['Command'] == six.text_type('true')
+        assert retrieved['Command'] == str('true')
         assert 'Image' in retrieved
         assert re.search(r'alpine:.*', retrieved['Image'])
         assert 'Status' in retrieved
@@ -104,9 +103,7 @@
         self.client.start(container3_id)
         assert self.client.wait(container3_id)['StatusCode'] == 0
 
-        logs = self.client.logs(container3_id)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container3_id).decode('utf-8')
         assert '{0}_NAME='.format(link_env_prefix1) in logs
         assert '{0}_ENV_FOO=1'.format(link_env_prefix1) in logs
         assert '{0}_NAME='.format(link_env_prefix2) in logs
@@ -227,9 +224,7 @@
         self.client.start(container)
         self.client.wait(container)
 
-        logs = self.client.logs(container)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container).decode('utf-8')
         groups = logs.strip().split(' ')
         assert '1000' in groups
         assert '1001' in groups
@@ -244,9 +239,7 @@
         self.client.start(container)
         self.client.wait(container)
 
-        logs = self.client.logs(container)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container).decode('utf-8')
 
         groups = logs.strip().split(' ')
         assert '1000' in groups
@@ -515,10 +508,7 @@
             TEST_IMG,
             ['ls', self.mount_dest],
         )
-        logs = self.client.logs(container)
-
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container).decode('utf-8')
         assert self.filename in logs
         inspect_data = self.client.inspect_container(container)
         self.check_container_data(inspect_data, True)
@@ -534,10 +524,8 @@
             TEST_IMG,
             ['ls', self.mount_dest],
         )
-        logs = self.client.logs(container)
+        logs = self.client.logs(container).decode('utf-8')
 
-        if six.PY3:
-            logs = logs.decode('utf-8')
         assert self.filename in logs
 
         inspect_data = self.client.inspect_container(container)
@@ -554,9 +542,7 @@
             host_config=host_config
         )
         assert container
-        logs = self.client.logs(container)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container).decode('utf-8')
         assert self.filename in logs
         inspect_data = self.client.inspect_container(container)
         self.check_container_data(inspect_data, True)
@@ -573,9 +559,7 @@
             host_config=host_config
         )
         assert container
-        logs = self.client.logs(container)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(container).decode('utf-8')
         assert self.filename in logs
         inspect_data = self.client.inspect_container(container)
         self.check_container_data(inspect_data, False)
@@ -645,9 +629,8 @@
             for d in strm:
                 destination.write(d)
             destination.seek(0)
-            retrieved_data = helpers.untar_file(destination, 'data.txt')
-            if six.PY3:
-                retrieved_data = retrieved_data.decode('utf-8')
+            retrieved_data = helpers.untar_file(destination, 'data.txt')\
+                .decode('utf-8')
             assert data == retrieved_data.strip()
 
     def test_get_file_stat_from_container(self):
@@ -683,9 +666,6 @@
         self.client.start(ctnr)
         self.client.wait(ctnr)
         logs = self.client.logs(ctnr)
-        if six.PY3:
-            logs = logs.decode('utf-8')
-            data = data.decode('utf-8')
         assert logs.strip() == data
 
     def test_copy_directory_to_container(self):
@@ -700,9 +680,7 @@
             self.client.put_archive(ctnr, '/vol1', test_tar)
         self.client.start(ctnr)
         self.client.wait(ctnr)
-        logs = self.client.logs(ctnr)
-        if six.PY3:
-            logs = logs.decode('utf-8')
+        logs = self.client.logs(ctnr).decode('utf-8')
         results = logs.strip().split()
         assert 'a.py' in results
         assert 'b.py' in results
@@ -861,7 +839,7 @@
         id = container['Id']
         self.tmp_containers.append(id)
         self.client.start(id)
-        logs = six.binary_type()
+        logs = b''
         for chunk in self.client.logs(id, stream=True, follow=True):
             logs += chunk
 
@@ -881,7 +859,7 @@
         id = container['Id']
         self.tmp_containers.append(id)
         self.client.start(id)
-        logs = six.binary_type()
+        logs = b''
 
         generator = self.client.logs(id, stream=True, follow=True)
         threading.Timer(1, generator.close).start()
diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py
index 37e26a3..d5f8989 100644
--- a/tests/integration/api_image_test.py
+++ b/tests/integration/api_image_test.py
@@ -7,9 +7,8 @@
 import threading
 
 import pytest
-import six
-from six.moves import BaseHTTPServer
-from six.moves import socketserver
+from http.server import SimpleHTTPRequestHandler
+import socketserver
 
 
 import docker
@@ -33,7 +32,7 @@
 
     def test_images_quiet(self):
         res1 = self.client.images(quiet=True)
-        assert type(res1[0]) == six.text_type
+        assert type(res1[0]) == str
 
 
 class PullImageTest(BaseAPIIntegrationTest):
@@ -44,7 +43,7 @@
             pass
         res = self.client.pull('hello-world')
         self.tmp_imgs.append('hello-world')
-        assert type(res) == six.text_type
+        assert type(res) == str
         assert len(self.client.images('hello-world')) >= 1
         img_info = self.client.inspect_image('hello-world')
         assert 'Id' in img_info
@@ -273,7 +272,7 @@
     def temporary_http_file_server(self, stream):
         '''Serve data from an IO stream over HTTP.'''
 
-        class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
+        class Handler(SimpleHTTPRequestHandler):
             def do_GET(self):
                 self.send_response(200)
                 self.send_header('Content-Type', 'application/x-tar')
diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py
index 7e5336e..1bee46e 100644
--- a/tests/integration/api_service_test.py
+++ b/tests/integration/api_service_test.py
@@ -5,7 +5,6 @@
 
 import docker
 import pytest
-import six
 
 from ..helpers import (
     force_leave_swarm, requires_api_version, requires_experimental
@@ -150,7 +149,7 @@
             else:
                 break
 
-        if six.PY3:
+        if log_line is not None:
             log_line = log_line.decode('utf-8')
         assert 'hello\n' in log_line
 
diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py
index 27e463d..4fd4d11 100644
--- a/tests/unit/fake_api.py
+++ b/tests/unit/fake_api.py
@@ -17,6 +17,8 @@
 FAKE_PATH = '/path'
 FAKE_VOLUME_NAME = 'perfectcherryblossom'
 FAKE_NODE_ID = '24ifsmvkjbyhk'
+FAKE_SECRET_ID = 'epdyrw4tsi03xy3deu8g8ly6o'
+FAKE_SECRET_NAME = 'super_secret'
 
 # Each method is prefixed with HTTP method (get, post...)
 # for clarity and readability
@@ -512,6 +514,12 @@
     return 200, None
 
 
+def post_fake_secret():
+    status_code = 200
+    response = {'ID': FAKE_SECRET_ID}
+    return status_code, response
+
+
 # Maps real api url to fake response callback
 prefix = 'http+docker://localhost'
 if constants.IS_WINDOWS_PLATFORM:
@@ -643,4 +651,6 @@
         CURRENT_VERSION, prefix, FAKE_NETWORK_ID
     ), 'POST'):
     post_fake_network_disconnect,
+    '{1}/{0}/secrets/create'.format(CURRENT_VERSION, prefix):
+    post_fake_secret,
 }
diff --git a/tests/unit/fake_api_client.py b/tests/unit/fake_api_client.py
index e85001d..5825b6e 100644
--- a/tests/unit/fake_api_client.py
+++ b/tests/unit/fake_api_client.py
@@ -40,6 +40,7 @@
             fake_api.post_fake_create_container()[1],
         'create_host_config.side_effect': api_client.create_host_config,
         'create_network.return_value': fake_api.post_fake_network()[1],
+        'create_secret.return_value': fake_api.post_fake_secret()[1],
         'exec_create.return_value': fake_api.post_fake_exec_create()[1],
         'exec_start.return_value': fake_api.post_fake_exec_start()[1],
         'images.return_value': fake_api.get_fake_images()[1],
diff --git a/tests/unit/models_secrets_test.py b/tests/unit/models_secrets_test.py
new file mode 100644
index 0000000..4ccf4c6
--- /dev/null
+++ b/tests/unit/models_secrets_test.py
@@ -0,0 +1,11 @@
+import unittest
+
+from .fake_api_client import make_fake_client
+from .fake_api import FAKE_SECRET_NAME
+
+
+class CreateServiceTest(unittest.TestCase):
+    def test_secrets_repr(self):
+        client = make_fake_client()
+        secret = client.secrets.create(name="super_secret", data="secret")
+        assert secret.__repr__() == "<Secret: '{}'>".format(FAKE_SECRET_NAME)
diff --git a/tests/unit/sshadapter_test.py b/tests/unit/sshadapter_test.py
index ddee592..874239a 100644
--- a/tests/unit/sshadapter_test.py
+++ b/tests/unit/sshadapter_test.py
@@ -2,31 +2,38 @@
 import docker
 from docker.transport.sshconn import SSHSocket
 
+
 class SSHAdapterTest(unittest.TestCase):
-    def test_ssh_hostname_prefix_trim(self):
-        conn = docker.transport.SSHHTTPAdapter(base_url="ssh://user@hostname:1234", shell_out=True)
+    @staticmethod
+    def test_ssh_hostname_prefix_trim():
+        conn = docker.transport.SSHHTTPAdapter(
+            base_url="ssh://user@hostname:1234", shell_out=True)
         assert conn.ssh_host == "user@hostname:1234"
 
-    def test_ssh_parse_url(self):
+    @staticmethod
+    def test_ssh_parse_url():
         c = SSHSocket(host="user@hostname:1234")
         assert c.host == "hostname"
         assert c.port == "1234"
         assert c.user == "user"
 
-    def test_ssh_parse_hostname_only(self):
+    @staticmethod
+    def test_ssh_parse_hostname_only():
         c = SSHSocket(host="hostname")
         assert c.host == "hostname"
-        assert c.port == None
-        assert c.user == None
+        assert c.port is None
+        assert c.user is None
 
-    def test_ssh_parse_user_and_hostname(self):
+    @staticmethod
+    def test_ssh_parse_user_and_hostname():
         c = SSHSocket(host="user@hostname")
         assert c.host == "hostname"
-        assert c.port == None
+        assert c.port is None
         assert c.user == "user"
 
-    def test_ssh_parse_hostname_and_port(self):
+    @staticmethod
+    def test_ssh_parse_hostname_and_port():
         c = SSHSocket(host="hostname:22")
         assert c.host == "hostname"
         assert c.port == "22"
-        assert c.user == None
\ No newline at end of file
+        assert c.user is None
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
index a53151c..0d6ff22 100644
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -541,6 +541,12 @@
         assert internal_port == ["2000"]
         assert external_port == [("2001:abcd:ef00::2", "1000")]
 
+    def test_split_port_with_ipv6_square_brackets_address(self):
+        internal_port, external_port = split_port(
+            "[2001:abcd:ef00::2]:1000:2000")
+        assert internal_port == ["2000"]
+        assert external_port == [("2001:abcd:ef00::2", "1000")]
+
     def test_split_port_invalid(self):
         with pytest.raises(ValueError):
             split_port("0.0.0.0:1000:2000:tcp")
diff --git a/tox.ini b/tox.ini
index df797f4..d35d41a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py35, py36, py37, flake8
+envlist = py36, py37, flake8
 skipsdist=True
 
 [testenv]