Skip to content

Commit 63a3dff

Browse files
authored
Handle pull events with complete progress details only (#6320)
* Handle pull events with complete progress details only Under certain circumstances, Docker seems to send pull events with incomplete progress details (i.e., missing 'current' or 'total' fields). In practise, we've observed an empty dictionary for progress details as well as missing 'total' field (while 'current' was present). All events were using Docker 28.3.3 using the old, default Docker graph backend. * Fix docstring/comment
1 parent fc8fc17 commit 63a3dff

File tree

2 files changed

+78
-20
lines changed

2 files changed

+78
-20
lines changed

supervisor/docker/interface.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ def _process_pull_image_log( # noqa: C901
310310
if (
311311
stage in {PullImageLayerStage.DOWNLOADING, PullImageLayerStage.EXTRACTING}
312312
and reference.progress_detail
313+
and reference.progress_detail.current is not None
314+
and reference.progress_detail.total is not None
313315
):
314316
job.update(
315317
progress=progress,

tests/docker/test_interface.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -445,28 +445,23 @@ async def test_install_progress_rounding_does_not_cause_misses(
445445
]
446446
coresys.docker.images.pull.return_value = AsyncIterator(logs)
447447

448-
with (
449-
patch.object(
450-
type(coresys.supervisor), "arch", PropertyMock(return_value="i386")
451-
),
452-
):
453-
# Schedule job so we can listen for the end. Then we can assert against the WS mock
454-
event = asyncio.Event()
455-
job, install_task = coresys.jobs.schedule_job(
456-
test_docker_interface.install,
457-
JobSchedulerOptions(),
458-
AwesomeVersion("1.2.3"),
459-
"test",
460-
)
448+
# Schedule job so we can listen for the end. Then we can assert against the WS mock
449+
event = asyncio.Event()
450+
job, install_task = coresys.jobs.schedule_job(
451+
test_docker_interface.install,
452+
JobSchedulerOptions(),
453+
AwesomeVersion("1.2.3"),
454+
"test",
455+
)
461456

462-
async def listen_for_job_end(reference: SupervisorJob):
463-
if reference.uuid != job.uuid:
464-
return
465-
event.set()
457+
async def listen_for_job_end(reference: SupervisorJob):
458+
if reference.uuid != job.uuid:
459+
return
460+
event.set()
466461

467-
coresys.bus.register_event(BusEvent.SUPERVISOR_JOB_END, listen_for_job_end)
468-
await install_task
469-
await event.wait()
462+
coresys.bus.register_event(BusEvent.SUPERVISOR_JOB_END, listen_for_job_end)
463+
await install_task
464+
await event.wait()
470465

471466
capture_exception.assert_not_called()
472467

@@ -664,3 +659,64 @@ async def listen_for_job_end(reference: SupervisorJob):
664659
assert job.done is True
665660
assert job.progress == 100
666661
capture_exception.assert_not_called()
662+
663+
664+
async def test_missing_total_handled_gracefully(
665+
coresys: CoreSys,
666+
test_docker_interface: DockerInterface,
667+
ha_ws_client: AsyncMock,
668+
capture_exception: Mock,
669+
):
670+
"""Test missing 'total' fields in progress details handled gracefully."""
671+
coresys.core.set_state(CoreState.RUNNING)
672+
673+
# Progress details with missing 'total' fields observed in real-world pulls
674+
logs = [
675+
{
676+
"status": "Pulling from home-assistant/odroid-n2-homeassistant",
677+
"id": "2025.7.1",
678+
},
679+
{"status": "Pulling fs layer", "progressDetail": {}, "id": "1e214cd6d7d0"},
680+
{
681+
"status": "Downloading",
682+
"progressDetail": {"current": 436480882},
683+
"progress": "[===================================================] 436.5MB/436.5MB",
684+
"id": "1e214cd6d7d0",
685+
},
686+
{"status": "Verifying Checksum", "progressDetail": {}, "id": "1e214cd6d7d0"},
687+
{"status": "Download complete", "progressDetail": {}, "id": "1e214cd6d7d0"},
688+
{
689+
"status": "Extracting",
690+
"progressDetail": {"current": 436480882},
691+
"progress": "[===================================================] 436.5MB/436.5MB",
692+
"id": "1e214cd6d7d0",
693+
},
694+
{"status": "Pull complete", "progressDetail": {}, "id": "1e214cd6d7d0"},
695+
{
696+
"status": "Digest: sha256:7d97da645f232f82a768d0a537e452536719d56d484d419836e53dbe3e4ec736"
697+
},
698+
{
699+
"status": "Status: Downloaded newer image for ghcr.io/home-assistant/odroid-n2-homeassistant:2025.7.1"
700+
},
701+
]
702+
coresys.docker.images.pull.return_value = AsyncIterator(logs)
703+
704+
# Schedule job so we can listen for the end. Then we can assert against the WS mock
705+
event = asyncio.Event()
706+
job, install_task = coresys.jobs.schedule_job(
707+
test_docker_interface.install,
708+
JobSchedulerOptions(),
709+
AwesomeVersion("1.2.3"),
710+
"test",
711+
)
712+
713+
async def listen_for_job_end(reference: SupervisorJob):
714+
if reference.uuid != job.uuid:
715+
return
716+
event.set()
717+
718+
coresys.bus.register_event(BusEvent.SUPERVISOR_JOB_END, listen_for_job_end)
719+
await install_task
720+
await event.wait()
721+
722+
capture_exception.assert_not_called()

0 commit comments

Comments
 (0)