mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-05 23:01:15 +02:00
Merge pull request #2983 from yueguobin/fix/docker-image-digest-translation
fix(docker): accept the pinned image id when the two daemons report different ids for the same content
This commit is contained in:
8 files changed
+259
-19
No files matched your search
@@ -20,9 +20,9 @@ API routes for images.
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Request, status
|
||||
from fastapi import APIRouter, Body, HTTPException, Query, Request, status
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from gns3server.compute.docker import Docker
|
||||
@@ -54,13 +54,23 @@ async def pull_docker_image(image: str = Body(..., embed=True, min_length=1, pat
|
||||
|
||||
|
||||
@router.post("/docker/images/load", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def load_docker_image(request: Request) -> None:
|
||||
async def load_docker_image(
|
||||
request: Request,
|
||||
image: Optional[str] = Query(None, pattern=r"^\S+$", description="Tag of the image in the archive"),
|
||||
digest: Optional[str] = Query(
|
||||
None, pattern=r"^\S+$", description="Image id reported by the controller host daemon"
|
||||
),
|
||||
) -> None:
|
||||
"""
|
||||
Load a Docker image into the Docker daemon from a docker save tar stream.
|
||||
|
||||
The optional image tag and digest let the compute remember which image the
|
||||
controller pushed and how the local daemon identifies it (see
|
||||
Docker.load_image).
|
||||
"""
|
||||
|
||||
docker_manager = Docker.instance()
|
||||
await docker_manager.load_image(request.stream())
|
||||
await docker_manager.load_image(request.stream(), image=image, digest=digest)
|
||||
|
||||
|
||||
@router.get("/dynamips/images")
|
||||
|
||||
@@ -66,6 +66,11 @@ class Docker(BaseManager):
|
||||
self._session = None
|
||||
self._api_version = DOCKER_MINIMUM_API_VERSION
|
||||
self._host_checked = False
|
||||
# Image name -> (controller digest, local id) for the images synced from
|
||||
# the controller. The controller pins the id its own daemon reports,
|
||||
# which is not comparable with the id this daemon reports for the same
|
||||
# content (see load_image()).
|
||||
self._image_id_translations = {}
|
||||
|
||||
def _select_node_class(self, **kwargs):
|
||||
"""
|
||||
@@ -419,11 +424,16 @@ class Docker(BaseManager):
|
||||
progress_callback(f"Success pulling image {image}")
|
||||
|
||||
@locking
|
||||
async def load_image(self, stream, progress_callback=None):
|
||||
async def load_image(self, stream, image=None, digest=None, progress_callback=None):
|
||||
"""
|
||||
Load a Docker image into the Docker daemon from a docker save tar stream
|
||||
|
||||
:param stream: An async iterable of bytes (the tar produced by docker save)
|
||||
:param image: Tag of the image in the archive. Together with digest, the
|
||||
(controller digest, local id) pair is remembered so that node creation
|
||||
can accept the digest the controller pinned for this image
|
||||
:param digest: Image id as reported by the Docker daemon on the controller
|
||||
host (the value the controller pins in the node create payload)
|
||||
:param progress_callback: A function that receive a log message about image load progress
|
||||
"""
|
||||
|
||||
@@ -470,6 +480,39 @@ class Docker(BaseManager):
|
||||
if progress_callback:
|
||||
progress_callback("Docker image loaded")
|
||||
|
||||
if image and digest:
|
||||
await self._remember_image_id_translation(image, digest)
|
||||
|
||||
async def _remember_image_id_translation(self, image, controller_digest):
|
||||
"""
|
||||
Remember how the local daemon identifies an image that was just loaded
|
||||
from the controller.
|
||||
|
||||
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 recording the pair lets
|
||||
DockerVM accept that pin for the image it actually received instead of
|
||||
asking for another full sync on every node creation.
|
||||
"""
|
||||
|
||||
try:
|
||||
image_infos = await self.query("GET", f"images/{image}/json")
|
||||
except DockerError as e:
|
||||
log.warning(f"Image '{image}' has been loaded but cannot be inspected: {e}")
|
||||
return
|
||||
local_id = image_infos.get("Id")
|
||||
if local_id:
|
||||
self._image_id_translations[image] = (controller_digest, local_id)
|
||||
|
||||
def image_id_translation(self, image):
|
||||
"""
|
||||
:returns: the (controller digest, local id) pair recorded the last time
|
||||
this image was loaded from the controller, None otherwise
|
||||
"""
|
||||
|
||||
return self._image_id_translations.get(image)
|
||||
|
||||
async def list_images(self):
|
||||
"""
|
||||
Gets Docker image list.
|
||||
|
||||
@@ -591,15 +591,20 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
|
||||
if image_infos is None:
|
||||
raise DockerError(f"Cannot get information for image '{self._image}', please try again.")
|
||||
|
||||
if self._image_digest and image_infos.get("Id") != self._image_digest:
|
||||
local_id = image_infos.get("Id")
|
||||
if self._image_digest and local_id != self._image_digest:
|
||||
# the tag exists but points to a different image (e.g. a moved :latest):
|
||||
# report it as missing so the controller re-syncs the expected version
|
||||
local_id = image_infos.get("Id")
|
||||
log.info(
|
||||
f"Image '{self._image}' version mismatch on this compute: "
|
||||
f"local id '{local_id}' != expected '{self._image_digest}'"
|
||||
)
|
||||
raise ImageMissingError(self._image)
|
||||
# report it as missing so the controller re-syncs the expected version.
|
||||
# Image ids are not comparable across image stores (a classic store
|
||||
# reports the config digest where a containerd store, or an image
|
||||
# loaded from an OCI archive, reports the manifest digest), so accept
|
||||
# the pin for the image this compute received from the controller.
|
||||
if self.manager.image_id_translation(self._image) != (self._image_digest, local_id):
|
||||
log.info(
|
||||
f"Image '{self._image}' version mismatch on this compute: "
|
||||
f"local id '{local_id}' != expected '{self._image_digest}'"
|
||||
)
|
||||
raise ImageMissingError(self._image)
|
||||
|
||||
available_cpus = psutil.cpu_count(logical=True)
|
||||
if self._cpus > available_cpus:
|
||||
|
||||
@@ -527,7 +527,9 @@ class Node:
|
||||
res = False
|
||||
if last_missing_image:
|
||||
try:
|
||||
res = await self._upload_missing_image(self._node_type, last_missing_image)
|
||||
res = await self._upload_missing_image(
|
||||
self._node_type, last_missing_image, data.get("image_digest")
|
||||
)
|
||||
except ControllerError as upload_error:
|
||||
# For Docker the image is pulled from a registry: a
|
||||
# failed pull (unknown tag, no network, private image)
|
||||
@@ -1075,14 +1077,18 @@ class Node:
|
||||
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs
|
||||
)
|
||||
|
||||
async def _upload_missing_image(self, type, img):
|
||||
async def _upload_missing_image(self, type, img, image_digest=None):
|
||||
"""
|
||||
Search an image on local computer and upload it to remote compute
|
||||
if the image exists
|
||||
|
||||
:param image_digest: digest pinned for this image in the create payload,
|
||||
forwarded so the compute can translate it (see
|
||||
_sync_missing_docker_image)
|
||||
"""
|
||||
|
||||
if self._node_type == "docker":
|
||||
return await self._sync_missing_docker_image(img)
|
||||
return await self._sync_missing_docker_image(img, image_digest)
|
||||
|
||||
image = self._find_local_image(type, img)
|
||||
if image is None:
|
||||
@@ -1119,11 +1125,17 @@ class Node:
|
||||
if image_id:
|
||||
data["image_digest"] = image_id
|
||||
|
||||
async def _sync_missing_docker_image(self, image):
|
||||
async def _sync_missing_docker_image(self, image, image_digest=None):
|
||||
"""
|
||||
Export a Docker image from the Docker daemon on the controller host and
|
||||
stream it to the remote compute, or ask the compute to pull it when the
|
||||
image is not available locally
|
||||
|
||||
:param image_digest: digest pinned for this image in the create payload.
|
||||
It is sent along with the archive so the compute can remember how its
|
||||
own daemon identifies the image it received: image ids are not
|
||||
comparable across image stores, so the pin alone would keep being
|
||||
reported as a mismatch after every sync.
|
||||
"""
|
||||
|
||||
from gns3server.compute.docker import Docker
|
||||
@@ -1147,8 +1159,11 @@ class Node:
|
||||
self.project.emit_notification(
|
||||
"log.info", {"message": f"Syncing Docker image '{image}' to compute '{self._compute.name}'"}
|
||||
)
|
||||
params = {"image": image}
|
||||
if image_digest:
|
||||
params["digest"] = image_digest
|
||||
try:
|
||||
await self._compute.post("/docker/images/load", data=response.content, timeout=None)
|
||||
await self._compute.post("/docker/images/load", data=response.content, timeout=None, params=params)
|
||||
finally:
|
||||
response.close()
|
||||
self.project.emit_notification(
|
||||
|
||||
Reference in new issue
Block a user