Adds controller and compute API support for explicitly pulling or updating Docker images.

This commit is contained in:
Cristi committed 2026-07-24 16:40:20 +03:00
1 parent 28f883b46a
commit 2abc7e09a3
6 files changed
+229 -32

No files matched your search

+11 -1
View File
@@ -21,7 +21,7 @@ API routes for images.
import os
import urllib.parse
from fastapi import APIRouter, Request, status, Response, HTTPException
from fastapi import APIRouter, Body, Request, status, Response, HTTPException
from fastapi.responses import FileResponse
from typing import List
@@ -43,6 +43,16 @@ async def get_docker_images() -> List[dict]:
return await docker_manager.list_images()
@router.post("/docker/images/pull", status_code=status.HTTP_204_NO_CONTENT)
async def pull_docker_image(image: str = Body(..., embed=True, min_length=1, pattern=r"^\S+$")) -> None:
"""
Pull or update a Docker image.
"""
docker_manager = Docker.instance()
await docker_manager.pull_image(image, force=True)
@router.get("/dynamips/images")
async def get_dynamips_images() -> List[dict]:
"""
+20 -1
View File
@@ -18,7 +18,7 @@
API routes for computes.
"""
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Body, Depends, status
from typing import Any, List, Union, Optional
from uuid import UUID
@@ -165,6 +165,25 @@ async def docker_get_images(compute_id: Union[str, UUID]) -> List[schemas.Comput
return result
@router.post(
"/{compute_id}/docker/images/pull",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Compute.Modify"))]
)
async def docker_pull_image(
compute_id: Union[str, UUID],
image: str = Body(..., embed=True, min_length=1, pattern=r"^\S+$")
) -> None:
"""
Pull or update a Docker image on a compute.
Required privilege: Compute.Modify
"""
compute = Controller.instance().get_compute(str(compute_id))
await compute.forward("POST", "docker", "images/pull", data={"image": image})
@router.get("/{compute_id}/virtualbox/vms", response_model=List[schemas.ComputeVirtualBoxVM])
async def virtualbox_vms(compute_id: Union[str, UUID]) -> List[schemas.ComputeVirtualBoxVM]:
"""
+46 -28
View File
@@ -260,19 +260,21 @@ class Docker(BaseManager):
return connection
@locking
async def pull_image(self, image, progress_callback=None):
async def pull_image(self, image, progress_callback=None, force=False):
"""
Pulls an image from the Docker repository
:params image: Image name
:params progress_callback: A function that receive a log message about image download progress
:params force: Pull the image even if it is already available locally
"""
try:
await self.query("GET", f"images/{image}/json")
return # We already have the image skip the download
except DockerHttp404Error:
pass
if not force:
try:
await self.query("GET", f"images/{image}/json")
return # We already have the image skip the download
except DockerHttp404Error:
pass
if progress_callback:
progress_callback(f"Pulling '{image}' from Docker repository")
@@ -285,29 +287,45 @@ class Docker(BaseManager):
)
# The pull api will stream status via an HTTP JSON stream
content = ""
while True:
try:
chunk = await response.content.read(CHUNK_SIZE)
except aiohttp.ServerDisconnectedError:
log.error(f"Disconnected from server while pulling Docker image '{image}' from Docker repository")
break
except asyncio.TimeoutError:
log.error("Timeout while pulling Docker image '{}' from Docker repository".format(image))
break
if not chunk:
break
content += chunk.decode("utf-8")
try:
while True:
try:
chunk = await response.content.read(CHUNK_SIZE)
except aiohttp.ServerDisconnectedError as e:
raise DockerError(
f"Disconnected while pulling Docker image '{image}' from Docker repository"
) from e
except asyncio.TimeoutError as e:
raise DockerError(
f"Timeout while pulling Docker image '{image}' from Docker repository"
) from e
if not chunk:
break
content += chunk.decode("utf-8")
try:
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
if not isinstance(answer, dict):
raise DockerError(f"Invalid response while pulling Docker image '{image}'")
error_detail = answer.get("errorDetail")
error = answer.get("error")
if not error and isinstance(error_detail, dict):
error = error_detail.get("message")
if error:
raise DockerError(error)
if "progress" in answer and progress_callback:
progress_callback("Pulling image {}:{}: {}".format(image, answer["id"], answer["progress"]))
content = content[index:]
except ValueError: # Partial JSON
pass
if content.strip():
raise DockerError(f"Invalid response while pulling Docker image '{image}'")
finally:
response.close()
try:
while True:
content = content.lstrip(" \r\n\t")
answer, index = json.JSONDecoder().raw_decode(content)
if "progress" in answer and progress_callback:
progress_callback("Pulling image {}:{}: {}".format(image, answer["id"], answer["progress"]))
content = content[index:]
except ValueError: # Partial JSON
pass
response.close()
if progress_callback:
progress_callback(f"Success pulling image {image}")