@@ -220,10 +220,12 @@ async def _docker_login(self, image: str) -> None:
220220
221221 await self .sys_run_in_executor (self .sys_docker .docker .login , ** credentials )
222222
223- def _process_pull_image_log (self , job_id : str , reference : PullLogEntry ) -> None :
223+ def _process_pull_image_log (
224+ self , install_job_id : str , reference : PullLogEntry
225+ ) -> None :
224226 """Process events fired from a docker while pulling an image, filtered to a given job id."""
225227 if (
226- reference .job_id != job_id
228+ reference .job_id != install_job_id
227229 or not reference .id
228230 or not reference .status
229231 or not (stage := PullImageLayerStage .from_status (reference .status ))
@@ -237,21 +239,22 @@ def _process_pull_image_log(self, job_id: str, reference: PullLogEntry) -> None:
237239 name = "Pulling container image layer" ,
238240 initial_stage = stage .status ,
239241 reference = reference .id ,
240- parent_id = job_id ,
242+ parent_id = install_job_id ,
243+ internal = True ,
241244 )
242245 job .done = False
243246 return
244247
245248 # Find our sub job to update details of
246249 for j in self .sys_jobs .jobs :
247- if j .parent_id == job_id and j .reference == reference .id :
250+ if j .parent_id == install_job_id and j .reference == reference .id :
248251 job = j
249252 break
250253
251254 # This likely only occurs if the logs came in out of sync and we got progress before the Pulling FS Layer one
252255 if not job :
253256 raise DockerLogOutOfOrder (
254- f"Received pull image log with status { reference .status } for image id { reference .id } and parent job { job_id } but could not find a matching job, skipping" ,
257+ f"Received pull image log with status { reference .status } for image id { reference .id } and parent job { install_job_id } but could not find a matching job, skipping" ,
255258 _LOGGER .debug ,
256259 )
257260
@@ -325,10 +328,56 @@ def _process_pull_image_log(self, job_id: str, reference: PullLogEntry) -> None:
325328 else job .extra ,
326329 )
327330
331+ # Once we have received a progress update for every child job, start to set status of the main one
332+ install_job = self .sys_jobs .get_job (install_job_id )
333+ layer_jobs = [
334+ job
335+ for job in self .sys_jobs .jobs
336+ if job .parent_id == install_job .uuid
337+ and job .name == "Pulling container image layer"
338+ ]
339+
340+ # First set the total bytes to be downloaded/extracted on the main job
341+ if not install_job .extra :
342+ total = 0
343+ for job in layer_jobs :
344+ if not job .extra :
345+ return
346+ total += job .extra ["total" ]
347+ install_job .extra = {"total" : total }
348+ else :
349+ total = install_job .extra ["total" ]
350+
351+ # Then determine total progress based on progress of each sub-job, factoring in size of each compared to total
352+ progress = 0.0
353+ stage = PullImageLayerStage .PULL_COMPLETE
354+ for job in layer_jobs :
355+ if not job .extra :
356+ return
357+ progress += job .progress * (job .extra ["total" ] / total )
358+ job_stage = PullImageLayerStage .from_status (cast (str , job .stage ))
359+
360+ if job_stage < PullImageLayerStage .EXTRACTING :
361+ stage = PullImageLayerStage .DOWNLOADING
362+ elif (
363+ stage == PullImageLayerStage .PULL_COMPLETE
364+ and job_stage < PullImageLayerStage .PULL_COMPLETE
365+ ):
366+ stage = PullImageLayerStage .EXTRACTING
367+
368+ # Ensure progress is 100 at this point to prevent float drift
369+ if stage == PullImageLayerStage .PULL_COMPLETE :
370+ progress = 100
371+
372+ # To reduce noise, limit updates to when result has changed by an entire percent or when stage changed
373+ if stage != install_job .stage or progress >= install_job .progress + 1 :
374+ install_job .update (stage = stage .status , progress = progress )
375+
328376 @Job (
329377 name = "docker_interface_install" ,
330378 on_condition = DockerJobError ,
331379 concurrency = JobConcurrency .GROUP_REJECT ,
380+ internal = True ,
332381 )
333382 async def install (
334383 self ,
@@ -351,11 +400,11 @@ async def install(
351400 # Try login if we have defined credentials
352401 await self ._docker_login (image )
353402
354- job_id = self .sys_jobs .current .uuid
403+ curr_job_id = self .sys_jobs .current .uuid
355404
356405 async def process_pull_image_log (reference : PullLogEntry ) -> None :
357406 try :
358- self ._process_pull_image_log (job_id , reference )
407+ self ._process_pull_image_log (curr_job_id , reference )
359408 except DockerLogOutOfOrder as err :
360409 # Send all these to sentry. Missing a few progress updates
361410 # shouldn't matter to users but matters to us
@@ -629,7 +678,10 @@ async def check_image(
629678 concurrency = JobConcurrency .GROUP_REJECT ,
630679 )
631680 async def update (
632- self , version : AwesomeVersion , image : str | None = None , latest : bool = False
681+ self ,
682+ version : AwesomeVersion ,
683+ image : str | None = None ,
684+ latest : bool = False ,
633685 ) -> None :
634686 """Update a Docker image."""
635687 image = image or self .image
0 commit comments