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 ComposeConfig, ComposeInspector, ComposeProjectRef, ComposeStatus, InfraError, InfraResult,
9 MutationResult, MutationVerification,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ComposeRecreateFingerprint {
15 pub project: String,
17 pub services: Vec<String>,
19 pub sha256: String,
21}
22
23impl ComposeRecreateFingerprint {
24 pub fn new(
26 project: impl Into<String>,
27 mut services: Vec<String>,
28 sha256: impl Into<String>,
29 ) -> InfraResult<Self> {
30 let project = project.into();
31 services.sort();
32 services.dedup();
33 let sha256 = sha256.into();
34 if project.is_empty() || services.is_empty() {
35 return Err(InfraError::InvalidRequest {
36 domain: "compose-recreate",
37 message: "project and at least one service are required".into(),
38 });
39 }
40 if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
41 return Err(InfraError::InvalidRequest {
42 domain: "compose-recreate",
43 message: "Compose fingerprint must be SHA-256 hex".into(),
44 });
45 }
46 Ok(Self {
47 project,
48 services,
49 sha256: sha256.to_ascii_lowercase(),
50 })
51 }
52}
53
54pub fn compose_recreate_fingerprint(
56 config: &ComposeConfig,
57 status: &ComposeStatus,
58) -> InfraResult<ComposeRecreateFingerprint> {
59 if config.project != status.project {
60 return Err(InfraError::InvalidRequest {
61 domain: "compose-recreate",
62 message: "Compose config and status project identities differ".into(),
63 });
64 }
65 let mut rows = status.services.clone();
66 rows.sort_by(|a, b| a.service.cmp(&b.service));
67 let services = config.services.keys().cloned().collect::<Vec<_>>();
68 let encoded = serde_json::to_vec(&(config, rows)).map_err(|error| InfraError::Parse {
69 domain: "compose-recreate",
70 message: error.to_string(),
71 })?;
72 ComposeRecreateFingerprint::new(
73 config.project.clone(),
74 services,
75 crate::mutation::sha256_hex(&encoded),
76 )
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct ComposeRecreateRequest {
82 operation_id: OperationId,
83 operation: OperationName,
84 project: ComposeProjectRef,
85 expected: ComposeRecreateFingerprint,
86 deadline: Timestamp,
87}
88
89impl ComposeRecreateRequest {
90 #[must_use]
92 pub fn new(
93 operation_id: OperationId,
94 operation: OperationName,
95 project: ComposeProjectRef,
96 expected: ComposeRecreateFingerprint,
97 deadline: Timestamp,
98 ) -> Self {
99 Self {
100 operation_id,
101 operation,
102 project,
103 expected,
104 deadline,
105 }
106 }
107 #[must_use]
109 pub fn operation_id(&self) -> &OperationId {
110 &self.operation_id
111 }
112 #[must_use]
114 pub fn operation(&self) -> &OperationName {
115 &self.operation
116 }
117 #[must_use]
119 pub const fn project(&self) -> &ComposeProjectRef {
120 &self.project
121 }
122 #[must_use]
124 pub const fn expected(&self) -> &ComposeRecreateFingerprint {
125 &self.expected
126 }
127 #[must_use]
129 pub const fn deadline(&self) -> Timestamp {
130 self.deadline
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct ComposeRecreateReceipt {
137 pub host: HostId,
139 pub topology_revision: TopologyRevision,
141 pub project: String,
143 pub send_state: MutationSendState,
145 pub stdout: String,
147 pub stderr: String,
149 pub output_truncated: bool,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct ComposeRecreateOutcome {
156 pub host: HostId,
158 pub topology_revision: TopologyRevision,
160 pub project: String,
162 pub before: ComposeStatus,
164 pub after: Option<ComposeStatus>,
166 pub changed: bool,
168 pub send_state: MutationSendState,
170 pub stdout: String,
172 pub stderr: String,
174 pub output_truncated: bool,
176 pub verification_status: VerificationStatus,
178 pub verification: MutationVerification,
180}
181
182#[async_trait]
184pub trait ComposeRecreateMutator: Send + Sync {
185 async fn recreate_compose(
187 &self,
188 host: &HostRecord,
189 request: &ComposeRecreateRequest,
190 cancellation: &CancellationToken,
191 ) -> MutationResult<ComposeRecreateReceipt>;
192}
193
194pub trait ComposeRecreateClient: ComposeInspector + ComposeRecreateMutator {}
196impl<T> ComposeRecreateClient for T where T: ComposeInspector + ComposeRecreateMutator {}
197
198#[cfg(test)]
199#[path = "compose_recreate_tests.rs"]
200mod tests;