@@ -228,6 +228,16 @@ impl ContainerBroker {
228228
229229 Request :: Pull { image, request_id } => self . pull_image ( & image, request_id) . await ,
230230
231+ Request :: Build {
232+ tag,
233+ dockerfile,
234+ context,
235+ request_id,
236+ } => {
237+ self . build_image ( & tag, & dockerfile, context, request_id)
238+ . await
239+ }
240+
231241 Request :: CopyFrom {
232242 container_id,
233243 path,
@@ -859,6 +869,171 @@ impl ContainerBroker {
859869 }
860870 }
861871
872+ /// Build an image from Dockerfile
873+ async fn build_image (
874+ & self ,
875+ tag : & str ,
876+ dockerfile_b64 : & str ,
877+ context_b64 : Option < String > ,
878+ request_id : String ,
879+ ) -> Response {
880+ use base64:: Engine ;
881+ use bollard:: image:: BuildImageOptions ;
882+
883+ // Verify policy allows building this tag
884+ // For now, only allow term-compiler images or specific tags
885+ // This is a basic check, could be expanded in SecurityPolicy
886+ if !tag. starts_with ( "term-compiler:" ) && !tag. starts_with ( "ghcr.io/" ) {
887+ let err = format ! ( "Image tag not allowed: {}" , tag) ;
888+ self . audit (
889+ AuditAction :: ImageBuild ,
890+ "" ,
891+ "" ,
892+ None ,
893+ false ,
894+ Some ( err. clone ( ) ) ,
895+ )
896+ . await ;
897+ return Response :: error ( request_id, ContainerError :: PolicyViolation ( err) ) ;
898+ }
899+
900+ // Prepare build context (tar archive)
901+ let mut tar_buffer = Vec :: new ( ) ;
902+
903+ // 1. If context provided, decode it (expecting tar/tar.gz)
904+ if let Some ( ctx) = context_b64 {
905+ match base64:: engine:: general_purpose:: STANDARD . decode ( ctx) {
906+ Ok ( data) => {
907+ tar_buffer = data;
908+ }
909+ Err ( e) => {
910+ return Response :: error (
911+ request_id,
912+ ContainerError :: InvalidConfig ( format ! ( "Invalid base64 context: {}" , e) ) ,
913+ ) ;
914+ }
915+ }
916+ }
917+
918+ // 2. Add Dockerfile to context if not empty
919+ // If tar_buffer is empty, create new tar. If not, we append or overwrite Dockerfile?
920+ // Simpler approach: Create a new tar containing just the Dockerfile if context is None.
921+ // If context is provided, assume it contains everything needed OR inject Dockerfile.
922+ // For term-compiler, we just need Dockerfile mostly.
923+
924+ // Let's decode Dockerfile
925+ let dockerfile_content =
926+ match base64:: engine:: general_purpose:: STANDARD . decode ( dockerfile_b64) {
927+ Ok ( d) => d,
928+ Err ( e) => {
929+ return Response :: error (
930+ request_id,
931+ ContainerError :: InvalidConfig ( format ! ( "Invalid base64 dockerfile: {}" , e) ) ,
932+ ) ;
933+ }
934+ } ;
935+
936+ // If no context provided, create a tar with just the Dockerfile
937+ if tar_buffer. is_empty ( ) {
938+ let mut builder = tar:: Builder :: new ( Vec :: new ( ) ) ;
939+
940+ let mut header = tar:: Header :: new_gnu ( ) ;
941+ header. set_size ( dockerfile_content. len ( ) as u64 ) ;
942+ header. set_mode ( 0o644 ) ;
943+ header. set_cksum ( ) ;
944+
945+ if let Err ( e) =
946+ builder. append_data ( & mut header, "Dockerfile" , dockerfile_content. as_slice ( ) )
947+ {
948+ return Response :: error (
949+ request_id,
950+ ContainerError :: InternalError ( format ! ( "Failed to create tar: {}" , e) ) ,
951+ ) ;
952+ }
953+
954+ if let Err ( e) = builder. finish ( ) {
955+ return Response :: error (
956+ request_id,
957+ ContainerError :: InternalError ( format ! ( "Failed to finish tar: {}" , e) ) ,
958+ ) ;
959+ }
960+
961+ tar_buffer = builder. into_inner ( ) . unwrap_or_default ( ) ;
962+ }
963+ // NOTE: If context IS provided, we assume it's a valid tar that contains Dockerfile
964+ // or we rely on the Dockerfile string being ignored?
965+ // Actually bollard/Docker API takes "dockerfile" option to specify filename,
966+ // but typically expects the file inside the tar.
967+ // For this implementation, we'll assume:
968+ // - If context is None: create tar with Dockerfile content named "Dockerfile"
969+ // - If context is Some: use it as is, ignore dockerfile_content (or expect client to have put it in context)
970+ // BUT the API has `dockerfile` param. Let's support the simple case first (no context).
971+
972+ let options = BuildImageOptions {
973+ t : tag,
974+ dockerfile : "Dockerfile" ,
975+ rm : true ,
976+ forcerm : true ,
977+ ..Default :: default ( )
978+ } ;
979+
980+ info ! ( tag = %tag, size = tar_buffer. len( ) , "Starting image build" ) ;
981+
982+ let mut stream = self
983+ . docker
984+ . build_image ( options, None , Some ( tar_buffer. into ( ) ) ) ;
985+ let mut logs = String :: new ( ) ;
986+ let mut image_id = String :: new ( ) ;
987+
988+ while let Some ( result) = stream. next ( ) . await {
989+ match result {
990+ Ok ( info) => {
991+ if let Some ( stream) = info. stream {
992+ logs. push_str ( & stream) ;
993+ }
994+ if let Some ( aux) = info. aux {
995+ if let Some ( id) = aux. id {
996+ image_id = id;
997+ }
998+ }
999+ if let Some ( error) = info. error {
1000+ logs. push_str ( & format ! ( "\n ERROR: {}" , error) ) ;
1001+ self . audit (
1002+ AuditAction :: ImageBuild ,
1003+ "" ,
1004+ "" ,
1005+ None ,
1006+ false ,
1007+ Some ( error. clone ( ) ) ,
1008+ )
1009+ . await ;
1010+ return Response :: error (
1011+ request_id,
1012+ ContainerError :: DockerError ( format ! ( "Build failed: {}" , error) ) ,
1013+ ) ;
1014+ }
1015+ }
1016+ Err ( e) => {
1017+ return Response :: error (
1018+ request_id,
1019+ ContainerError :: DockerError ( format ! ( "Build error: {}" , e) ) ,
1020+ ) ;
1021+ }
1022+ }
1023+ }
1024+
1025+ info ! ( tag = %tag, id = %image_id, "Image built successfully" ) ;
1026+
1027+ self . audit ( AuditAction :: ImageBuild , "" , "" , Some ( & image_id) , true , None )
1028+ . await ;
1029+
1030+ Response :: Built {
1031+ image_id,
1032+ logs,
1033+ request_id,
1034+ }
1035+ }
1036+
8621037 /// Copy a file from container using Docker archive API
8631038 /// Returns base64-encoded file contents
8641039 async fn copy_from_container (
0 commit comments