Skip to main content

soma_infra/
image_build_engine.rs

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/// Borrowed services required for one image build execution.
12#[derive(Clone, Copy)]
13pub struct ImageBuildServices<'a> {
14    /// Descriptor-confined context inspector.
15    pub contexts: &'a dyn BuildContextInspector,
16    /// Image build mutation driver.
17    pub mutator: &'a dyn ImageBuildMutator,
18    /// Docker image-store reader used for verification.
19    pub images: &'a dyn ImageReader,
20}
21
22/// Coordinates context verification, image build, and image-store verification.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct ImageBuildEngine;
25
26impl ImageBuildEngine {
27    /// Builds one image from an unchanged context and verifies the resulting identity.
28    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;