1use soma_fleet::HostRecord;
2use soma_ops::{MutationSendState, VerificationStatus};
3use tokio_util::sync::CancellationToken;
4
5use crate::{
6 BuildContextInspector, ImageBuildMutator, ImageBuildOutcome, ImageBuildRequest,
7 ImageListOptions, ImageReader, MutationFailure, MutationProgressReporter, MutationResult,
8 MutationVerification,
9};
10
11#[derive(Clone, Copy)]
13pub struct ImageBuildServices<'a> {
14 pub contexts: &'a dyn BuildContextInspector,
16 pub mutator: &'a dyn ImageBuildMutator,
18 pub images: &'a dyn ImageReader,
20}
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct ImageBuildEngine;
25
26impl ImageBuildEngine {
27 pub async fn execute(
29 &self,
30 services: ImageBuildServices<'_>,
31 host: &HostRecord,
32 request: &ImageBuildRequest,
33 progress: &dyn MutationProgressReporter,
34 cancellation: &CancellationToken,
35 ) -> MutationResult<ImageBuildOutcome> {
36 ensure_admitted(request, cancellation)?;
37 let actual = services
38 .contexts
39 .fingerprint(host, request.context(), request.deadline(), cancellation)
40 .await
41 .map_err(|error| MutationFailure::new(MutationSendState::NotSent, error))?;
42 if &actual != request.expected_context() {
43 return Err(MutationFailure::new(
44 MutationSendState::NotSent,
45 crate::InfraError::InvalidRequest {
46 domain: "image-build",
47 message: "build context changed after planning".into(),
48 },
49 ));
50 }
51 let before = services
52 .images
53 .list_images(host, &ImageListOptions::default(), cancellation)
54 .await
55 .map_err(|error| MutationFailure::new(MutationSendState::NotSent, error))?;
56 let before = crate::image_pull_engine::find_image(&before, request.tag());
57 let receipt = services
58 .mutator
59 .build_image(host, request, progress, cancellation)
60 .await?;
61 let after_read = services
62 .images
63 .list_images(host, &ImageListOptions::default(), cancellation)
64 .await;
65 let (after, verification_status, summary) = match after_read {
66 Ok(images) => match crate::image_pull_engine::find_image(&images, request.tag()) {
67 Some(image) => (
68 Some(image),
69 VerificationStatus::Verified,
70 "the requested build tag resolves to a local image identity".into(),
71 ),
72 None => (
73 None,
74 VerificationStatus::Failed,
75 "the build command completed but the output tag was not found locally".into(),
76 ),
77 },
78 Err(error) => (
79 None,
80 VerificationStatus::Inconclusive,
81 format!("the build command completed but image verification failed: {error}"),
82 ),
83 };
84 let changed = match (&before, &after) {
85 (Some(before), Some(after)) => before.id != after.id,
86 (None, Some(_)) => true,
87 _ => false,
88 };
89 Ok(ImageBuildOutcome {
90 host: host.id().clone(),
91 topology_revision: host.revision().clone(),
92 tag: request.tag().to_owned(),
93 context: actual,
94 before,
95 after,
96 changed,
97 send_state: receipt.send_state,
98 stdout: receipt.stdout,
99 stderr: receipt.stderr,
100 output_truncated: receipt.output_truncated,
101 progress_delivery_errors: receipt.progress_delivery_errors,
102 verification_status,
103 verification: MutationVerification {
104 status: format!("{verification_status:?}").to_ascii_lowercase(),
105 summary,
106 },
107 })
108 }
109}
110
111fn ensure_admitted(
112 request: &ImageBuildRequest,
113 cancellation: &CancellationToken,
114) -> MutationResult<()> {
115 if cancellation.is_cancelled() {
116 return Err(MutationFailure::new(
117 MutationSendState::NotSent,
118 soma_fleet::FleetError::Cancelled.into(),
119 ));
120 }
121 if soma_ops::Timestamp::now() >= request.deadline() {
122 return Err(MutationFailure::new(
123 MutationSendState::NotSent,
124 soma_fleet::FleetError::DeadlineExceeded.into(),
125 ));
126 }
127 Ok(())
128}
129
130#[cfg(test)]
131#[path = "image_build_engine_tests.rs"]
132mod tests;