docker: don't recreate containers on empty-string property PUTs

Web clients serialize empty form fields as "" while unset values are
stored as None on the node. The bare != diff in the update handler then
sees a phantom change on every full PUT and recreates the container for
nothing -- even when the user only changed a controller-only field such
as netmiko_device_type.

Normalize at the schema boundary ("" -> None for start_command,
environment and extra_hosts; "" -> "/" for console_http_path), make
the setters apply the same canonicalization, and create nodes through
the setters instead of bypassing them in __init__ so both paths store
identical values.
This commit is contained in:
YueGuobin committed 2026-08-21 21:41:47 +08:00
1 parent c3145a9f65
commit 210103058f
3 files changed
+68 -8

No files matched your search

@@ -306,6 +306,45 @@ class TestDockerNodesRoutes:
assert response.json()["environment"] == "GNS3=1\nGNS4=0"
assert response.json()["extra_hosts"] == "test:127.0.0.1"
async def test_docker_update_empty_strings_do_not_recreate_container(
self,
app: FastAPI,
compute_client: AsyncClient,
compute_project: Project
) -> None:
"""
Web clients serialize empty form fields as "" while unset values are
stored as None on the node: a full PUT must not see a phantom change
and recreate the container for nothing.
"""
params = {"name": "DOCKER-EMPTY", "image": "nginx", "environment": ""}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
response = await compute_client.post(
app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["environment"] is None # "" normalized at creation
assert response.json()["console_http_path"] == "/"
node_id = response.json()["node_id"]
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock:
response = await compute_client.put(
app.url_path_for("compute:update_docker_node", project_id=compute_project.id, node_id=node_id),
json={
"name": "DOCKER-EMPTY",
"start_command": "",
"environment": "",
"extra_hosts": "",
"console_http_path": "",
},
)
assert response.status_code == 200
assert not mock.called # no real change: the container must not be recreated
assert response.json()["start_command"] is None
assert response.json()["console_http_path"] == "/"
async def test_docker_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: