Skip to main content

soma_infra/
compose_pull.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use soma_fleet::{HostId, HostRecord, TopologyRevision};
4use soma_ops::{MutationSendState, OperationId, OperationName, Timestamp, VerificationStatus};
5use tokio_util::sync::CancellationToken;
6
7use crate::{
8    ComposeInspector, ComposeProjectRef, ImageIdentity, InfraError, MutationProgressReporter,
9    MutationResult, MutationVerification,
10};
11
12const MAX_SERVICE_CHARS: usize = 128;
13
14/// Deadline-bound Compose image pull request.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ComposePullRequest {
17    operation_id: OperationId,
18    operation: OperationName,
19    project: ComposeProjectRef,
20    service: Option<String>,
21    deadline: Timestamp,
22}
23
24impl ComposePullRequest {
25    /// Creates a validated Compose pull request.
26    pub fn new(
27        operation_id: OperationId,
28        operation: OperationName,
29        project: ComposeProjectRef,
30        service: Option<String>,
31        deadline: Timestamp,
32    ) -> Result<Self, InfraError> {
33        if let Some(service) = &service {
34            validate_service(service)?;
35        }
36        Ok(Self {
37            operation_id,
38            operation,
39            project,
40            service,
41            deadline,
42        })
43    }
44
45    /// Returns the operation identity.
46    #[must_use]
47    pub fn operation_id(&self) -> &OperationId {
48        &self.operation_id
49    }
50    /// Returns the canonical operation name.
51    #[must_use]
52    pub fn operation(&self) -> &OperationName {
53        &self.operation
54    }
55    /// Returns the Compose project.
56    #[must_use]
57    pub const fn project(&self) -> &ComposeProjectRef {
58        &self.project
59    }
60    /// Returns the optional service filter.
61    #[must_use]
62    pub fn service(&self) -> Option<&str> {
63        self.service.as_deref()
64    }
65    /// Returns the absolute deadline.
66    #[must_use]
67    pub const fn deadline(&self) -> Timestamp {
68        self.deadline
69    }
70}
71
72/// Receipt returned when the Compose pull command completes.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ComposePullReceipt {
75    /// Target host.
76    pub host: HostId,
77    /// Exact topology revision.
78    pub topology_revision: TopologyRevision,
79    /// Project name.
80    pub project: String,
81    /// Optional service filter.
82    pub service: Option<String>,
83    /// Backend send state.
84    pub send_state: MutationSendState,
85    /// Bounded progress delivery failures.
86    pub progress_delivery_errors: Vec<String>,
87    /// Whether command output was truncated.
88    pub output_truncated: bool,
89}
90
91/// One Compose service image verification row.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ComposePulledImage {
94    /// Compose service name.
95    pub service: String,
96    /// Configured image reference.
97    pub reference: String,
98    /// Image identity before the pull.
99    pub before: Option<ImageIdentity>,
100    /// Image identity after the pull.
101    pub after: Option<ImageIdentity>,
102    /// Whether the verified image content identity changed.
103    pub changed: bool,
104    /// Whether the configured reference resolved locally after the pull.
105    pub verified: bool,
106}
107
108/// Verified Compose pull outcome.
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ComposePullOutcome {
111    /// Target host.
112    pub host: HostId,
113    /// Exact topology revision.
114    pub topology_revision: TopologyRevision,
115    /// Compose project name.
116    pub project: String,
117    /// Optional service filter.
118    pub service: Option<String>,
119    /// Backend send state.
120    pub send_state: MutationSendState,
121    /// Per-service image verification.
122    pub images: Vec<ComposePulledImage>,
123    /// Whether any verified image identity changed.
124    pub changed: bool,
125    /// Progress sink failures that did not alter execution truth.
126    pub progress_delivery_errors: Vec<String>,
127    /// Whether command output was truncated.
128    pub output_truncated: bool,
129    /// Independent verification status.
130    pub verification_status: VerificationStatus,
131    /// Verification explanation.
132    pub verification: MutationVerification,
133}
134
135/// Driver for one Compose image pull command.
136#[async_trait]
137pub trait ComposePullMutator: Send + Sync {
138    /// Pulls configured service images while preserving send uncertainty.
139    async fn pull_compose_images(
140        &self,
141        host: &HostRecord,
142        request: &ComposePullRequest,
143        progress: &dyn MutationProgressReporter,
144        cancellation: &CancellationToken,
145    ) -> MutationResult<ComposePullReceipt>;
146}
147
148/// Complete Compose client required by the pull coordinator.
149pub trait ComposePullClient: ComposeInspector + ComposePullMutator {}
150impl<T> ComposePullClient for T where T: ComposeInspector + ComposePullMutator {}
151
152pub(crate) fn validate_service(service: &str) -> Result<(), InfraError> {
153    let count = service.chars().count();
154    if count == 0
155        || count > MAX_SERVICE_CHARS
156        || service.starts_with('-')
157        || !service
158            .chars()
159            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
160    {
161        Err(InfraError::InvalidRequest {
162            domain: "compose-pull",
163            message: "invalid Compose service name".into(),
164        })
165    } else {
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171#[path = "compose_pull_tests.rs"]
172mod tests;