soma_infra/
image_pull_engine.rs1use soma_fleet::{HostRecord, TopologyRevision};
2use soma_ops::{MutationSendState, VerificationStatus};
3use tokio_util::sync::CancellationToken;
4
5use crate::{
6 DockerArtifactClient, ImageIdentity, ImageListOptions, ImagePullOutcome, ImagePullRequest,
7 ImageSummary, MutationFailure, MutationProgressReporter, MutationResult, MutationVerification,
8};
9
10#[derive(Debug, Clone, Copy, Default)]
12pub struct ImagePullEngine;
13
14impl ImagePullEngine {
15 pub async fn execute(
17 &self,
18 client: &dyn DockerArtifactClient,
19 host: &HostRecord,
20 request: &ImagePullRequest,
21 progress: &dyn MutationProgressReporter,
22 cancellation: &CancellationToken,
23 ) -> MutationResult<ImagePullOutcome> {
24 ensure_admitted(request, cancellation)?;
25 let before = client
26 .list_images(host, &ImageListOptions::default(), cancellation)
27 .await
28 .map_err(|error| MutationFailure::new(MutationSendState::NotSent, error))?;
29 let before = find_image(&before, request.image());
30 let receipt = client
31 .pull_image(host, request, progress, cancellation)
32 .await?;
33 let after_read = client
34 .list_images(host, &ImageListOptions::default(), cancellation)
35 .await;
36 let (after, verification_status, summary) = match after_read {
37 Ok(images) => match find_image(&images, request.image()) {
38 Some(image) => (
39 Some(image),
40 VerificationStatus::Verified,
41 "the requested image reference resolves to a local content identity".into(),
42 ),
43 None => (
44 None,
45 VerificationStatus::Failed,
46 "the pull stream completed but the requested image reference was not found locally"
47 .into(),
48 ),
49 },
50 Err(error) => (
51 None,
52 VerificationStatus::Inconclusive,
53 format!("the pull stream completed but image verification failed: {error}"),
54 ),
55 };
56 let changed = match (&before, &after) {
57 (Some(before), Some(after)) => before.id != after.id,
58 (None, Some(_)) => true,
59 _ => false,
60 };
61 Ok(ImagePullOutcome {
62 host: host.id().clone(),
63 topology_revision: TopologyRevision::clone(host.revision()),
64 image: request.image().to_owned(),
65 changed,
66 send_state: receipt.send_state,
67 before,
68 after,
69 total_events: receipt.total_events,
70 progress: receipt.progress,
71 progress_truncated: receipt.progress_truncated,
72 progress_delivery_errors: receipt.progress_delivery_errors,
73 verification_status,
74 verification: MutationVerification {
75 status: verification_status_text(verification_status),
76 summary,
77 },
78 })
79 }
80}
81
82fn ensure_admitted(
83 request: &ImagePullRequest,
84 cancellation: &CancellationToken,
85) -> MutationResult<()> {
86 if cancellation.is_cancelled() {
87 return Err(MutationFailure::new(
88 MutationSendState::NotSent,
89 soma_fleet::FleetError::Cancelled.into(),
90 ));
91 }
92 if soma_ops::Timestamp::now() >= request.deadline() {
93 return Err(MutationFailure::new(
94 MutationSendState::NotSent,
95 soma_fleet::FleetError::DeadlineExceeded.into(),
96 ));
97 }
98 Ok(())
99}
100
101pub(crate) fn find_image(images: &[ImageSummary], reference: &str) -> Option<ImageIdentity> {
102 let canonical = crate::canonical_image_reference(reference);
103 images
104 .iter()
105 .find(|image| {
106 image.id == reference
107 || image.repo_tags.iter().any(|tag| tag == &canonical)
108 || image.repo_digests.iter().any(|digest| digest == reference)
109 })
110 .map(|image| ImageIdentity {
111 id: image.id.clone(),
112 repo_tags: image.repo_tags.clone(),
113 repo_digests: image.repo_digests.clone(),
114 })
115}
116
117fn verification_status_text(status: VerificationStatus) -> String {
118 format!("{status:?}").to_ascii_lowercase()
119}
120
121#[cfg(test)]
122#[path = "image_pull_engine_tests.rs"]
123mod tests;