feat: sync Docker images from the controller to remote computes

When a Docker node is created on a remote compute whose Docker daemon
does not have the image, the compute now raises ImageMissingError
instead of blindly pulling from the Docker repository. The controller
exports the image from the Docker daemon on its host (docker save
stream) and streams it to the compute which loads it, so locally built
or docker-loaded images work across computes. When the image is not
available on the controller host either, the compute is asked to pull
it from the Docker repository as a fallback.

- add a POST /docker/images/load compute endpoint that streams a
  docker save tar into the Docker daemon
- let Docker.http_query pass raw (non-dict) request bodies through so
  the tar can be streamed to the daemon
- drop the inline pull from DockerVM.create() and the now unused
  DockerVM.pull_image wrapper
This commit is contained in:
YueGuobin committed 2026-09-14 00:36:54 +08:00
1 parent 2a3626824f
commit c477812332
8 files changed
+215 -75

No files matched your search

+34 -1
View File
@@ -21,7 +21,8 @@ import uuid
import os
from unittest.mock import MagicMock, ANY
from tests.utils import AsyncioMagicMock
from tests.utils import AsyncioMagicMock, asyncio_patch
from gns3server.compute.docker.docker_error import DockerError
from gns3server.controller.node import Node
from gns3server.controller.project import Project
@@ -693,6 +694,38 @@ async def test_upload_missing_image(compute, controller, images_dir):
compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None)
@pytest.mark.asyncio
async def test_sync_missing_docker_image_from_controller_daemon(compute, controller):
project = Project(str(uuid.uuid4()), controller=controller)
node = Node(project, compute, "demo",
node_id=str(uuid.uuid4()),
node_type="docker",
properties={"image": "gns3/frr:latest"})
response = MagicMock()
response.content = MagicMock()
with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response) as save_mock:
assert await node._upload_missing_image("docker", "gns3/frr:latest") is True
save_mock.assert_called_with("GET", "images/gns3/frr:latest/get", timeout=None)
compute.post.assert_called_with("/docker/images/load", data=response.content, timeout=None)
response.close.assert_called_once_with()
@pytest.mark.asyncio
async def test_sync_missing_docker_image_pull_fallback(compute, controller):
project = Project(str(uuid.uuid4()), controller=controller)
node = Node(project, compute, "demo",
node_id=str(uuid.uuid4()),
node_type="docker",
properties={"image": "nginx:latest"})
with asyncio_patch("gns3server.compute.docker.Docker.http_query", side_effect=DockerError("404")):
assert await node._upload_missing_image("docker", "nginx:latest") is True
compute.post.assert_called_with("/docker/images/pull", data={"image": "nginx:latest"}, timeout=None)
def test_update_label(node):
"""
The text in label need to be always the