Skip to content

Commit 6da49e2

Browse files
committed
feat(broker): add image build support via Build command
1 parent eb3065c commit 6da49e2

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

crates/secure-container-runtime/src/broker.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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!("\nERROR: {}", 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(

crates/secure-container-runtime/src/protocol.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ pub enum Request {
7272
/// Pull an image
7373
Pull { image: String, request_id: String },
7474

75+
/// Build an image from Dockerfile
76+
Build {
77+
/// Tag for the image (e.g. "term-compiler:latest")
78+
tag: String,
79+
/// Dockerfile content (base64 encoded)
80+
dockerfile: String,
81+
/// Build context (optional tar.gz, base64 encoded)
82+
context: Option<String>,
83+
request_id: String,
84+
},
85+
7586
/// Health check
7687
Ping { request_id: String },
7788

@@ -108,6 +119,7 @@ impl Request {
108119
Request::List { request_id, .. } => request_id,
109120
Request::Logs { request_id, .. } => request_id,
110121
Request::Pull { request_id, .. } => request_id,
122+
Request::Build { request_id, .. } => request_id,
111123
Request::Ping { request_id, .. } => request_id,
112124
Request::CopyFrom { request_id, .. } => request_id,
113125
Request::CopyTo { request_id, .. } => request_id,
@@ -126,6 +138,7 @@ impl Request {
126138
Request::List { .. } => "list",
127139
Request::Logs { .. } => "logs",
128140
Request::Pull { .. } => "pull",
141+
Request::Build { .. } => "build",
129142
Request::Ping { .. } => "ping",
130143
Request::CopyFrom { .. } => "copy_from",
131144
Request::CopyTo { .. } => "copy_to",
@@ -206,6 +219,13 @@ pub enum Response {
206219
/// Image pulled
207220
Pulled { image: String, request_id: String },
208221

222+
/// Image built
223+
Built {
224+
image_id: String,
225+
logs: String,
226+
request_id: String,
227+
},
228+
209229
/// Pong response
210230
Pong { version: String, request_id: String },
211231

@@ -240,6 +260,7 @@ impl Response {
240260
Response::ContainerList { request_id, .. } => request_id,
241261
Response::LogsResult { request_id, .. } => request_id,
242262
Response::Pulled { request_id, .. } => request_id,
263+
Response::Built { request_id, .. } => request_id,
243264
Response::Pong { request_id, .. } => request_id,
244265
Response::CopyFromResult { request_id, .. } => request_id,
245266
Response::CopyToResult { request_id, .. } => request_id,

crates/secure-container-runtime/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ pub enum AuditAction {
219219
ContainerRemove,
220220
ContainerExec,
221221
ImagePull,
222+
ImageBuild,
222223
PolicyViolation,
223224
}
224225

0 commit comments

Comments
 (0)