mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
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:
8 files changed
+215
-75
No files matched your search
@@ -255,6 +255,55 @@ async def test_pull_image_propagates_timeout():
|
||||
response.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_image():
|
||||
|
||||
class Content:
|
||||
|
||||
def __init__(self):
|
||||
self._chunks = [b'{"stream": "Loaded image ID: sha256:e90e34656806"}', b""]
|
||||
|
||||
async def read(self, size):
|
||||
return self._chunks.pop(0)
|
||||
|
||||
response = MagicMock()
|
||||
response.content = Content()
|
||||
|
||||
async def tar_stream():
|
||||
yield b"tar-bytes"
|
||||
|
||||
stream = tar_stream()
|
||||
messages = []
|
||||
with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response) as mock:
|
||||
await Docker.instance().load_image(stream, progress_callback=messages.append)
|
||||
mock.assert_called_with("POST", "images/load", data=stream, timeout=None)
|
||||
response.close.assert_called_once()
|
||||
assert any("Loaded image" in message for message in messages)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_image_error():
|
||||
|
||||
class Content:
|
||||
|
||||
def __init__(self):
|
||||
self._chunks = [b'{"error": "invalid tar file"}', b""]
|
||||
|
||||
async def read(self, size):
|
||||
return self._chunks.pop(0)
|
||||
|
||||
response = MagicMock()
|
||||
response.content = Content()
|
||||
|
||||
async def tar_stream():
|
||||
yield b"not-a-tar"
|
||||
|
||||
with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response):
|
||||
with pytest.raises(DockerError, match="invalid tar file"):
|
||||
await Docker.instance().load_image(tar_stream())
|
||||
response.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_docker_check_connection_docker_minimum_version(vm):
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from gns3server.compute.ubridge.ubridge_error import UbridgeNamespaceError
|
||||
from gns3server.compute.compute_error import ComputeError
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error
|
||||
from gns3server.compute.error import ImageMissingError
|
||||
from gns3server.compute.docker import Docker
|
||||
|
||||
|
||||
@@ -555,65 +556,12 @@ async def test_create_environment_with_last_new_line_character(compute_project,
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_image_not_available(compute_project, manager):
|
||||
|
||||
call = 0
|
||||
async def information():
|
||||
nonlocal call
|
||||
if call == 0:
|
||||
call += 1
|
||||
raise DockerHttp404Error("missing")
|
||||
else:
|
||||
return {}
|
||||
|
||||
response = {
|
||||
"Id": "e90e34656806",
|
||||
"Warnings": []
|
||||
}
|
||||
|
||||
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu")
|
||||
vm._get_image_information = MagicMock()
|
||||
vm._get_image_information.side_effect = information
|
||||
with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull:
|
||||
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
|
||||
vm._get_image_information = MagicMock(side_effect=DockerHttp404Error("missing"))
|
||||
with asyncio_patch("gns3server.compute.docker.Docker.query") as query_mock:
|
||||
with pytest.raises(ImageMissingError, match="ubuntu:latest"):
|
||||
await vm.create()
|
||||
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
|
||||
"Tty": True,
|
||||
"OpenStdin": True,
|
||||
"StdinOnce": False,
|
||||
"HostConfig":
|
||||
{
|
||||
"CapAdd": ["ALL"],
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": Docker.resources_path(),
|
||||
"Target": "/gns3",
|
||||
"ReadOnly": True
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": os.path.join(vm.working_dir, "etc", "network"),
|
||||
"Target": "/gns3volumes/etc/network"
|
||||
}
|
||||
],
|
||||
"Privileged": True,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"UsernsMode": "host"
|
||||
},
|
||||
"Volumes": {},
|
||||
"NetworkDisabled": True,
|
||||
"Hostname": "test",
|
||||
"Image": "ubuntu:latest",
|
||||
"Env": [
|
||||
"container=docker",
|
||||
"GNS3_MAX_ETHERNET=eth0",
|
||||
"GNS3_VOLUMES=/etc/network"
|
||||
],
|
||||
"Entrypoint": ["/gns3/init.sh"],
|
||||
"Cmd": ["/bin/sh"]
|
||||
})
|
||||
assert vm._cid == "e90e34656806"
|
||||
mock_pull.assert_called_with("ubuntu:latest")
|
||||
query_mock.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in new issue
Block a user