soma_infra/
compose_pull.rs1use 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#[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 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 #[must_use]
47 pub fn operation_id(&self) -> &OperationId {
48 &self.operation_id
49 }
50 #[must_use]
52 pub fn operation(&self) -> &OperationName {
53 &self.operation
54 }
55 #[must_use]
57 pub const fn project(&self) -> &ComposeProjectRef {
58 &self.project
59 }
60 #[must_use]
62 pub fn service(&self) -> Option<&str> {
63 self.service.as_deref()
64 }
65 #[must_use]
67 pub const fn deadline(&self) -> Timestamp {
68 self.deadline
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ComposePullReceipt {
75 pub host: HostId,
77 pub topology_revision: TopologyRevision,
79 pub project: String,
81 pub service: Option<String>,
83 pub send_state: MutationSendState,
85 pub progress_delivery_errors: Vec<String>,
87 pub output_truncated: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct ComposePulledImage {
94 pub service: String,
96 pub reference: String,
98 pub before: Option<ImageIdentity>,
100 pub after: Option<ImageIdentity>,
102 pub changed: bool,
104 pub verified: bool,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ComposePullOutcome {
111 pub host: HostId,
113 pub topology_revision: TopologyRevision,
115 pub project: String,
117 pub service: Option<String>,
119 pub send_state: MutationSendState,
121 pub images: Vec<ComposePulledImage>,
123 pub changed: bool,
125 pub progress_delivery_errors: Vec<String>,
127 pub output_truncated: bool,
129 pub verification_status: VerificationStatus,
131 pub verification: MutationVerification,
133}
134
135#[async_trait]
137pub trait ComposePullMutator: Send + Sync {
138 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
148pub 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;