99 CompleteSandboxImageBuildParams ,
1010 CreateSandboxParams ,
1111 CreateSandboxImageBuildParams ,
12+ ReuseSandboxDockerImageParams ,
1213 SandboxDetail ,
1314 SandboxExecParams ,
1415 SandboxExposeParams ,
1516 SandboxExposeResult ,
1617 SandboxImageBuild ,
1718 SandboxImageBuildCreateResult ,
19+ SandboxDockerImageReuseResult ,
1820 SandboxImageBuildListParams ,
1921 SandboxImageBuildListResponse ,
2022 SandboxImageListParams ,
3941from ....types import (
4042 CompleteSandboxImageBuildParams as CompleteSandboxImageBuildParamsDict ,
4143 CreateSandboxImageBuildParams as CreateSandboxImageBuildParamsDict ,
44+ ReuseSandboxDockerImageParams as ReuseSandboxDockerImageParamsDict ,
4245 CreateSandboxParams as CreateSandboxParamsDict ,
4346 SandboxExecParams as SandboxExecParamsDict ,
4447 SandboxExposeParams as SandboxExposeParamsDict ,
6770 build_docker_image_from_dockerfile ,
6871 is_terminal_image_build_status ,
6972 make_temp_docker_tag ,
70- package_docker_image ,
73+ merge_image_init ,
74+ package_docker_build_context_manifest ,
75+ package_docker_image_manifest ,
76+ prepare_docker_image_manifest_source ,
7177 remove_docker_image ,
72- upload_image_build_artifact ,
78+ upload_missing_image_build_artifacts ,
7379)
7480from .sandboxes .sandbox_files import (
7581 DEFAULT_WATCH_TIMEOUT_MS ,
@@ -494,6 +500,20 @@ async def get_image_build(self, build_id: str) -> SandboxImageBuild:
494500 payload = await self ._request ("GET" , f"/images/builds/{ build_id } " )
495501 return SandboxImageBuild (** payload ["build" ])
496502
503+ async def reuse_docker_image (
504+ self ,
505+ params : Union [
506+ ReuseSandboxDockerImageParamsDict ,
507+ ReuseSandboxDockerImageParams ,
508+ ],
509+ ) -> SandboxDockerImageReuseResult :
510+ payload = await self ._request (
511+ "POST" ,
512+ "/images/builds/reuse" ,
513+ data = dump_request (params , ReuseSandboxDockerImageParams ),
514+ )
515+ return SandboxDockerImageReuseResult (** payload )
516+
497517 async def complete_image_build (
498518 self ,
499519 build_id : str ,
@@ -566,63 +586,206 @@ async def build_image_from_docker_image(
566586 temp_dir : Optional [str ] = None ,
567587 upload_timeout : Optional [float ] = None ,
568588 ) -> SandboxImageBuild :
569- artifact = await _run_blocking (
570- package_docker_image ,
589+ source = await _run_blocking (
590+ prepare_docker_image_manifest_source ,
571591 docker_image ,
572592 platform = platform ,
593+ )
594+ try :
595+ explicit_image_init = (
596+ coerce_request (image_init , SandboxImageInit , name = "image_init" )
597+ if image_init is not None
598+ else None
599+ )
600+ normalized_image_init = merge_image_init (
601+ source .image_init ,
602+ explicit_image_init ,
603+ )
604+ normalized_image_config_user = (
605+ image_config_user
606+ if image_config_user is not None
607+ else source .image_config_user
608+ )
609+ try :
610+ reused = await self .reuse_docker_image (
611+ ReuseSandboxDockerImageParams (
612+ image_name = image_name ,
613+ source_image_digest = source .image_digest ,
614+ source_platform = platform ,
615+ image_config_user = normalized_image_config_user ,
616+ image_init = normalized_image_init ,
617+ )
618+ )
619+ except HyperbrowserError as error :
620+ if error .status_code != 404 :
621+ raise
622+ reused = None
623+ if reused is not None and reused .hit :
624+ if reused .build is None :
625+ raise RuntimeError (
626+ "exact image cache response is missing its completed build"
627+ )
628+ return reused .build
629+
630+ packaged = await _run_blocking (
631+ package_docker_image_manifest ,
632+ docker_image ,
633+ source .image_digest ,
634+ source .config ,
635+ platform = platform ,
636+ temp_dir = temp_dir ,
637+ )
638+ build_id = None
639+ build_started = False
640+ try :
641+ artifact = packaged .artifact
642+ create_result = await self .create_image_build (
643+ CreateSandboxImageBuildParams (
644+ image_name = image_name ,
645+ input_sha256 = artifact .sha256_hex ,
646+ input_size_bytes = artifact .size_bytes ,
647+ input_format = artifact .input_format ,
648+ source_platform = artifact .source_platform ,
649+ image_config_user = normalized_image_config_user ,
650+ image_init = normalized_image_init ,
651+ docker_image_manifest = packaged .manifest ,
652+ )
653+ )
654+ build_id = create_result .build .id
655+ await _run_blocking (
656+ upload_missing_image_build_artifacts ,
657+ create_result .uploads ,
658+ packaged .layers ,
659+ label = "Docker image layer" ,
660+ timeout = upload_timeout ,
661+ )
662+ build = await self ._complete_image_build_resilient (
663+ build_id ,
664+ artifact ,
665+ )
666+ build_started = True
667+ if wait :
668+ return await self .wait_for_image_build (
669+ build .id ,
670+ poll_interval = poll_interval ,
671+ timeout = wait_timeout ,
672+ )
673+ return build
674+ except Exception :
675+ if build_id is not None and not build_started :
676+ try :
677+ await self .cancel_image_build (build_id )
678+ except Exception :
679+ pass
680+ raise
681+ finally :
682+ await _run_blocking (packaged .cleanup )
683+ finally :
684+ await _run_blocking (source .cleanup )
685+
686+ async def _complete_image_build_resilient (
687+ self ,
688+ build_id : str ,
689+ artifact ,
690+ ) -> SandboxImageBuild :
691+ params = CompleteSandboxImageBuildParams (
692+ input_sha256 = artifact .sha256_hex ,
693+ input_size_bytes = artifact .size_bytes ,
694+ input_format = artifact .input_format ,
695+ )
696+ try :
697+ return await self .complete_image_build (build_id , params )
698+ except HyperbrowserError as exc :
699+ if exc .status_code != 409 :
700+ raise
701+ if "already in progress" in str (exc ).lower ():
702+ return await self .get_image_build (build_id )
703+ current = await self .get_image_build (build_id )
704+ if current .status not in ("awaiting_upload" , "upload_verified" ):
705+ raise
706+ await asyncio .sleep (2 )
707+ return await self .complete_image_build (build_id , params )
708+
709+ async def _build_image_from_remote_dockerfile (
710+ self ,
711+ * ,
712+ context_path ,
713+ image_name : str ,
714+ dockerfile ,
715+ platform : str ,
716+ remote_full_context : bool ,
717+ image_init : Optional [Union [SandboxImageInitDict , SandboxImageInit ]],
718+ image_config_user : Optional [str ],
719+ wait : bool ,
720+ poll_interval : float ,
721+ wait_timeout : Optional [float ],
722+ temp_dir : Optional [str ],
723+ upload_timeout : Optional [float ],
724+ ) -> SandboxImageBuild :
725+ packaged = await _run_blocking (
726+ package_docker_build_context_manifest ,
727+ context_path ,
728+ dockerfile = dockerfile ,
729+ force_full_context = remote_full_context ,
573730 temp_dir = temp_dir ,
574731 )
732+ build_id = None
733+ build_started = False
575734 try :
576735 normalized_image_init = (
577736 coerce_request (image_init , SandboxImageInit , name = "image_init" )
578737 if image_init is not None
579- else artifact . image_init
738+ else None
580739 )
740+ artifact = packaged .artifact
581741 create_result = await self .create_image_build (
582742 CreateSandboxImageBuildParams (
583743 image_name = image_name ,
584744 input_sha256 = artifact .sha256_hex ,
585745 input_size_bytes = artifact .size_bytes ,
586746 input_format = artifact .input_format ,
587747 source_platform = artifact .source_platform ,
588- image_config_user = (
589- image_config_user
590- if image_config_user is not None
591- else artifact .image_config_user
592- ),
748+ dockerfile_path = packaged .manifest .dockerfile_path ,
749+ image_config_user = image_config_user ,
593750 image_init = normalized_image_init ,
751+ context_manifest = packaged .manifest ,
594752 )
595753 )
754+ build_id = create_result .build .id
596755 await _run_blocking (
597- upload_image_build_artifact ,
598- create_result .upload ,
599- artifact .path ,
756+ upload_missing_image_build_artifacts ,
757+ create_result .uploads ,
758+ packaged .bundles ,
759+ label = "build context bundle" ,
600760 timeout = upload_timeout ,
601761 )
602- build = await self .complete_image_build (
603- create_result .build .id ,
604- CompleteSandboxImageBuildParams (
605- input_sha256 = artifact .sha256_hex ,
606- input_size_bytes = artifact .size_bytes ,
607- input_format = artifact .input_format ,
608- ),
609- )
762+ build = await self ._complete_image_build_resilient (build_id , artifact )
763+ build_started = True
610764 if wait :
611765 return await self .wait_for_image_build (
612766 build .id ,
613767 poll_interval = poll_interval ,
614768 timeout = wait_timeout ,
615769 )
616770 return build
771+ except Exception :
772+ if build_id is not None and not build_started :
773+ try :
774+ await self .cancel_image_build (build_id )
775+ except Exception :
776+ pass
777+ raise
617778 finally :
618- artifact .cleanup ( )
779+ await _run_blocking ( packaged .cleanup )
619780
620781 async def build_image_from_dockerfile (
621782 self ,
622783 * ,
623784 context_path ,
624785 image_name : str ,
625786 dockerfile = "Dockerfile" ,
787+ remote : bool = True ,
788+ remote_full_context : bool = False ,
626789 docker_tag : Optional [str ] = None ,
627790 platform : str = IMAGE_BUILD_SOURCE_PLATFORM ,
628791 build_args : Optional [Dict [str , str ]] = None ,
@@ -634,6 +797,26 @@ async def build_image_from_dockerfile(
634797 temp_dir : Optional [str ] = None ,
635798 upload_timeout : Optional [float ] = None ,
636799 ) -> SandboxImageBuild :
800+ if remote :
801+ if docker_tag is not None or build_args :
802+ raise ValueError (
803+ "docker_tag and build_args require remote=False; remote "
804+ "Dockerfile builds send the build context to Hyperbrowser"
805+ )
806+ return await self ._build_image_from_remote_dockerfile (
807+ context_path = context_path ,
808+ image_name = image_name ,
809+ dockerfile = dockerfile ,
810+ platform = platform ,
811+ remote_full_context = remote_full_context ,
812+ image_init = image_init ,
813+ image_config_user = image_config_user ,
814+ wait = wait ,
815+ poll_interval = poll_interval ,
816+ wait_timeout = wait_timeout ,
817+ temp_dir = temp_dir ,
818+ upload_timeout = upload_timeout ,
819+ )
637820 tag = docker_tag or make_temp_docker_tag ()
638821 remove_tag = docker_tag is None
639822 try :
0 commit comments