From f2aaf5ab38cf91992b316fa1a0a50c5316957b1c Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 17 Nov 2025 22:26:18 +1000 Subject: [PATCH] Docker API version requirements and process to handle older daemons --- gns3server/compute/docker/__init__.py | 41 ++++++++++++++++----------- tests/compute/docker/test_docker.py | 28 ++++++++++++++---- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index f6cb215fa..696646ade 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -39,9 +39,9 @@ log = logging.getLogger(__name__) # Be careful to keep it consistent -DOCKER_MINIMUM_API_VERSION = "1.25" -DOCKER_MINIMUM_VERSION = "1.13" -DOCKER_PREFERRED_API_VERSION = "1.30" +DOCKER_MINIMUM_API_VERSION = "1.40" +DOCKER_MINIMUM_VERSION = "19.03.8" +DOCKER_PREFERRED_API_VERSION = "1.44" CHUNK_SIZE = 1024 * 8 # 8KB @@ -123,22 +123,30 @@ class Docker(BaseManager): if not self._connected: try: self._connected = True - connector = self.connector() - version = await self.query("GET", "version") + self._connector = self.connector() + docker_info = await self.query("GET", "version") except (aiohttp.ClientOSError, FileNotFoundError): self._connected = False - raise DockerError("Can't connect to docker daemon") + raise DockerError("Can't connect to Docker daemon") - docker_version = parse_version(version['ApiVersion']) + api_version = parse_version(docker_info['ApiVersion']) + version = docker_info["Version"] - if docker_version < parse_version(DOCKER_MINIMUM_API_VERSION): - raise DockerError( - "Docker version is {}. GNS3 requires a minimum version of {}".format( - version["Version"], DOCKER_MINIMUM_VERSION)) + if api_version < parse_version(DOCKER_MINIMUM_API_VERSION): + raise DockerError("Docker version is {}. GNS3 requires a minimum version of {}".format( + version, + DOCKER_MINIMUM_VERSION) + ) preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION) - if docker_version >= preferred_api_version: + if api_version >= preferred_api_version: self._api_version = DOCKER_PREFERRED_API_VERSION + else: + # use the Min API version supported by the daemon + self._api_version = docker_info['MinAPIVersion'] + log.warning("Using Docker client with the minimum API version {}".format(self._api_version)) + + log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version)) def connector(self): @@ -186,7 +194,7 @@ class Docker(BaseManager): :param method: HTTP method :param path: Endpoint in API - :param data: Dictionnary with the body. Will be transformed to a JSON + :param data: Dictionary with the body. Will be transformed to a JSON :param params: Parameters added as a query arg :param timeout: Timeout :returns: HTTP response @@ -197,12 +205,11 @@ class Docker(BaseManager): timeout = 60 * 60 * 24 * 31 # One month timeout if path == 'version': - url = "http://docker/v1.24/" + path + url = "http://docker/" + path else: - url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path + await self._check_connection() # version is use by check connection + url = "http://docker/v" + self._api_version + "/" + path try: - if path != "version": # version is use by check connection - await self._check_connection() if self._session is None or self._session.closed: connector = self.connector() self._session = aiohttp.ClientSession(connector=connector) diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index 12e473425..9e411503d 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -47,7 +47,7 @@ async def test_query_success(vm): vm._session.request = AsyncioMagicMock(return_value=response) data = await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -69,7 +69,7 @@ async def test_query_error(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -89,7 +89,7 @@ async def test_query_error_json(vm): with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) vm._session.request.assert_called_with('POST', - 'http://docker/v1.25/test', + 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), data='{"a": true}', headers={'content-type': 'application/json'}, params={'b': 1}, @@ -180,7 +180,9 @@ async def test_docker_check_connection_docker_minimum_version(vm): async def test_docker_check_connection_docker_preferred_version_against_newer(vm): response = { - 'ApiVersion': '1.31' + 'ApiVersion': '1.52', + 'Version': '29.0.1', + } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -193,7 +195,9 @@ async def test_docker_check_connection_docker_preferred_version_against_newer(vm async def test_docker_check_connection_docker_preferred_version_against_older(vm): response = { - 'ApiVersion': '1.27', + 'ApiVersion': '1.43', + 'Version': '24.0.2', + 'MinAPIVersion': '1.40' } with patch("gns3server.compute.docker.Docker.connector"), \ @@ -203,6 +207,20 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm assert vm._api_version == DOCKER_MINIMUM_API_VERSION +async def test_docker_check_connection_docker_unsupported_version(vm): + + response = { + 'ApiVersion': '1.25', + 'Version': '1.13.1', + } + + with patch("gns3server.compute.docker.Docker.connector"), \ + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + vm._connected = False + with pytest.raises(DockerError) as e: + await vm._check_connection() + + @pytest.mark.asyncio async def test_install_busybox():