mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-19 01:12:22 +03:00
Docker ids are not comparable across image stores: for the same content a classic store reports the config digest while a containerd store (or an image loaded from an OCI archive) reports the manifest digest. The controller pins the id of its own daemon, so a compute whose store reports another id for that content answered ImageMissingError forever: every node creation streamed the image again (save/load round trip) without ever converging, then failed with "The image 'x' is missing" after the six retries. Docker 29 makes this easy to hit, since the id a daemon reports now depends on how the image entered it. The load endpoint now takes the tag and the digest pinned by the controller, and the compute records how its own daemon identifies the image it just received. Node creation accepts the pin through that pair, and only while the tag still resolves to the synced content: a tag moved to some other image, or an image the controller never pushed here, still counts as missing. The pair lives in the compute process, so a restarted compute syncs once more. Computes that predate the new endpoint parameters keep the previous behaviour, as do controllers that do not send them.
70 lines
3.1 KiB
Python
70 lines
3.1 KiB
Python
#
|
|
# Copyright (C) 2026 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import pytest
|
|
from fastapi import FastAPI, status
|
|
from httpx import AsyncClient
|
|
|
|
from tests.utils import asyncio_patch
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
class TestImagesRoutes:
|
|
async def test_pull_docker_image(self, app: FastAPI, compute_client: AsyncClient) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.Docker.pull_image") as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:pull_docker_image"), json={"image": "nginx:latest"}
|
|
)
|
|
mock.assert_called_once_with("nginx:latest", force=True)
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
@pytest.mark.parametrize("image", ["", " ", "nginx latest"])
|
|
async def test_pull_docker_image_rejects_invalid_name(
|
|
self, app: FastAPI, compute_client: AsyncClient, image: str
|
|
) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.Docker.pull_image") as mock:
|
|
response = await compute_client.post(app.url_path_for("compute:pull_docker_image"), json={"image": image})
|
|
mock.assert_not_called()
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
|
|
|
async def test_load_docker_image(self, app: FastAPI, compute_client: AsyncClient) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.Docker.load_image") as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:load_docker_image"), content=b"docker-save-tar-bytes"
|
|
)
|
|
mock.assert_called_once()
|
|
# the tar is streamed to the Docker daemon as an async iterable
|
|
stream = mock.call_args[0][0]
|
|
assert hasattr(stream, "__aiter__")
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_load_docker_image_with_image_and_digest(self, app: FastAPI, compute_client: AsyncClient) -> None:
|
|
|
|
digest = "sha256:" + "a" * 64
|
|
with asyncio_patch("gns3server.compute.docker.Docker.load_image") as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:load_docker_image"),
|
|
params={"image": "gns3/frr:latest", "digest": digest},
|
|
content=b"docker-save-tar-bytes",
|
|
)
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
# the tag and the digest pinned by the controller reach the manager
|
|
assert mock.call_args[1] == {"image": "gns3/frr:latest", "digest": digest}
|