1use crate::{
14 sha256_hex, ActivationReceipt, ArtifactAuthenticityVerifier, ArtifactDescriptor, ArtifactStore,
15 StagedArtifact, UpdateError, UpdateProvider, UpdateRequest, UpdateResult,
16};
17use semver::Version;
18
19#[cfg(test)]
20struct TestOnlyArtifactVerifier;
21
22#[cfg(test)]
23impl ArtifactAuthenticityVerifier for TestOnlyArtifactVerifier {
24 fn verify(&self, _artifact: &ArtifactDescriptor) -> UpdateResult<()> {
25 Ok(())
26 }
27}
28
29#[cfg(test)]
30static TEST_ONLY_ARTIFACTS: TestOnlyArtifactVerifier = TestOnlyArtifactVerifier;
32
33struct VerifiedArtifact {
34 descriptor: ArtifactDescriptor,
35 bytes: Vec<u8>,
36}
37
38struct ActivatedArtifact {
39 descriptor: ArtifactDescriptor,
40 receipt: ActivationReceipt,
41}
42
43pub trait ActivationHealthCheck: Send + Sync {
45 fn check(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()>;
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum UpdateFaultPoint {
52 AfterSelection,
54 AfterVerification,
56 AfterStaging,
58 AfterActivation,
60 BeforeCommit,
62}
63
64pub trait UpdateFaultInjector: Send + Sync {
66 fn check(&self, point: UpdateFaultPoint) -> UpdateResult<()>;
68}
69
70#[derive(Debug, Clone, Copy, Default)]
72pub struct NoUpdateFaults;
73
74impl UpdateFaultInjector for NoUpdateFaults {
75 fn check(&self, _point: UpdateFaultPoint) -> UpdateResult<()> {
76 Ok(())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum UpdateOutcome {
83 NoUpdate,
85 Applied(ArtifactDescriptor),
87 RolledBack {
89 attempted: ArtifactDescriptor,
91 reason: String,
93 },
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum UpdatePreparation {
99 NoUpdate,
101 AwaitingHealth(Box<ArtifactDescriptor>),
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum UpdateStaging {
108 NoUpdate,
110 Staged(Box<StagedArtifact>),
112}
113
114pub struct UpdateCoordinator<'a> {
116 provider: &'a dyn UpdateProvider,
117 store: &'a dyn ArtifactStore,
118 health: Option<&'a dyn ActivationHealthCheck>,
119 authenticity: &'a dyn ArtifactAuthenticityVerifier,
120 max_artifact_bytes: usize,
121}
122
123impl<'a> UpdateCoordinator<'a> {
124 pub fn new(
126 provider: &'a dyn UpdateProvider,
127 store: &'a dyn ArtifactStore,
128 health: &'a dyn ActivationHealthCheck,
129 max_artifact_bytes: usize,
130 ) -> UpdateResult<Self> {
131 #[cfg(test)]
132 {
133 Self::new_with_authenticity(
134 provider,
135 store,
136 health,
137 &TEST_ONLY_ARTIFACTS,
138 max_artifact_bytes,
139 )
140 }
141 #[cfg(not(test))]
142 {
143 let _ = (provider, store, health, max_artifact_bytes);
144 Err(UpdateError::Authenticity(
145 "an explicit artifact authenticity verifier is required".to_string(),
146 ))
147 }
148 }
149
150 pub fn new_with_authenticity(
152 provider: &'a dyn UpdateProvider,
153 store: &'a dyn ArtifactStore,
154 health: &'a dyn ActivationHealthCheck,
155 authenticity: &'a dyn ArtifactAuthenticityVerifier,
156 max_artifact_bytes: usize,
157 ) -> UpdateResult<Self> {
158 if max_artifact_bytes == 0 {
159 return Err(UpdateError::InvalidArtifact(
160 "max_artifact_bytes must be greater than zero".to_string(),
161 ));
162 }
163 Ok(Self {
164 provider,
165 store,
166 health: Some(health),
167 authenticity,
168 max_artifact_bytes,
169 })
170 }
171
172 pub fn new_for_preparation(
174 provider: &'a dyn UpdateProvider,
175 store: &'a dyn ArtifactStore,
176 authenticity: &'a dyn ArtifactAuthenticityVerifier,
177 max_artifact_bytes: usize,
178 ) -> UpdateResult<Self> {
179 if max_artifact_bytes == 0 {
180 return Err(UpdateError::InvalidArtifact(
181 "max_artifact_bytes must be greater than zero".to_string(),
182 ));
183 }
184 Ok(Self {
185 provider,
186 store,
187 health: None,
188 authenticity,
189 max_artifact_bytes,
190 })
191 }
192
193 pub fn apply(
195 &self,
196 request: &UpdateRequest,
197 runtime_version: &str,
198 protocol_version: &str,
199 ) -> UpdateResult<UpdateOutcome> {
200 self.apply_with_faults(request, runtime_version, protocol_version, &NoUpdateFaults)
201 }
202
203 pub fn prepare(
208 &self,
209 request: &UpdateRequest,
210 runtime_version: &str,
211 protocol_version: &str,
212 ) -> UpdateResult<UpdatePreparation> {
213 match self.stage_candidate(request, runtime_version, protocol_version)? {
214 UpdateStaging::NoUpdate => Ok(UpdatePreparation::NoUpdate),
215 UpdateStaging::Staged(staged) => {
216 let receipt = self.store.activate(*staged)?;
217 Ok(UpdatePreparation::AwaitingHealth(Box::new(
218 receipt.activated,
219 )))
220 }
221 }
222 }
223
224 pub fn stage_candidate(
226 &self,
227 request: &UpdateRequest,
228 runtime_version: &str,
229 protocol_version: &str,
230 ) -> UpdateResult<UpdateStaging> {
231 self.store.recover()?;
232 let Some(candidate) = self.select_candidate(request, runtime_version, protocol_version)?
233 else {
234 return Ok(UpdateStaging::NoUpdate);
235 };
236 let verified = self.fetch_verified(candidate)?;
237 let staged = self.store.stage(&verified.descriptor, &verified.bytes)?;
238 Ok(UpdateStaging::Staged(Box::new(staged)))
239 }
240
241 pub fn apply_with_faults(
243 &self,
244 request: &UpdateRequest,
245 runtime_version: &str,
246 protocol_version: &str,
247 faults: &dyn UpdateFaultInjector,
248 ) -> UpdateResult<UpdateOutcome> {
249 self.store.recover()?;
250 let Some(candidate) = self.select_candidate(request, runtime_version, protocol_version)?
251 else {
252 return Ok(UpdateOutcome::NoUpdate);
253 };
254 faults.check(UpdateFaultPoint::AfterSelection)?;
255 let verified = self.fetch_verified(candidate)?;
256 faults.check(UpdateFaultPoint::AfterVerification)?;
257 let activated = self.stage_and_activate(verified, faults)?;
258 self.finish_activation(activated, faults)
259 }
260
261 fn select_candidate(
262 &self,
263 request: &UpdateRequest,
264 runtime_version: &str,
265 protocol_version: &str,
266 ) -> UpdateResult<Option<ArtifactDescriptor>> {
267 let Some(candidate) = self.provider.latest(request)? else {
268 return Ok(None);
269 };
270 if candidate.application_id() != &request.application_id {
271 return Err(UpdateError::Incompatible(
272 "artifact application identity differs from the request".to_string(),
273 ));
274 }
275 if candidate.channel() != request.channel {
276 return Err(UpdateError::Incompatible(
277 "artifact update channel differs from the request".to_string(),
278 ));
279 }
280 self.ensure_upgrade(request, &candidate)?;
281 candidate.ensure_compatible(runtime_version, protocol_version)?;
282 Ok(Some(candidate))
283 }
284
285 fn ensure_upgrade(
286 &self,
287 request: &UpdateRequest,
288 candidate: &ArtifactDescriptor,
289 ) -> UpdateResult<()> {
290 let installed = Version::parse(&request.current_version).map_err(|error| {
291 UpdateError::Incompatible(format!("invalid installed application version: {error}"))
292 })?;
293 let candidate_version =
294 Version::parse(candidate.application_version()).map_err(|error| {
295 UpdateError::InvalidArtifact(format!(
296 "invalid candidate application version: {error}"
297 ))
298 })?;
299 if candidate_version <= installed {
300 return Err(UpdateError::Incompatible(format!(
301 "candidate version {candidate_version} does not advance installed version {installed}"
302 )));
303 }
304 let Some(active) = self.store.current()? else {
305 return Ok(());
306 };
307 if active.application_id() != candidate.application_id() {
308 return Err(UpdateError::Incompatible(
309 "active artifact application identity differs from the candidate".to_string(),
310 ));
311 }
312 if active.build_id() == candidate.build_id() {
313 return Err(UpdateError::Incompatible(
314 "candidate reuses the active build identity".to_string(),
315 ));
316 }
317 let active_version = Version::parse(active.application_version()).map_err(|error| {
318 UpdateError::Store(format!("active artifact version is invalid: {error}"))
319 })?;
320 if candidate_version <= active_version {
321 return Err(UpdateError::Incompatible(format!(
322 "candidate version {candidate_version} does not advance active version {active_version}"
323 )));
324 }
325 Ok(())
326 }
327
328 fn fetch_verified(&self, candidate: ArtifactDescriptor) -> UpdateResult<VerifiedArtifact> {
329 let declared_size =
330 usize::try_from(candidate.size_bytes()).map_err(|_| UpdateError::ArtifactTooLarge {
331 max_bytes: self.max_artifact_bytes,
332 })?;
333 if declared_size > self.max_artifact_bytes {
334 return Err(UpdateError::ArtifactTooLarge {
335 max_bytes: self.max_artifact_bytes,
336 });
337 }
338 let bytes = self.provider.fetch(&candidate, self.max_artifact_bytes)?;
339 if bytes.len() > self.max_artifact_bytes || bytes.len() != declared_size {
340 return Err(UpdateError::ArtifactTooLarge {
341 max_bytes: self.max_artifact_bytes,
342 });
343 }
344 if sha256_hex(&bytes) != candidate.sha256() {
345 return Err(UpdateError::ChecksumMismatch);
346 }
347 self.authenticity.verify(&candidate)?;
348 Ok(VerifiedArtifact {
349 descriptor: candidate,
350 bytes,
351 })
352 }
353
354 fn stage_and_activate(
355 &self,
356 verified: VerifiedArtifact,
357 faults: &dyn UpdateFaultInjector,
358 ) -> UpdateResult<ActivatedArtifact> {
359 let staged = self.store.stage(&verified.descriptor, &verified.bytes)?;
360 faults.check(UpdateFaultPoint::AfterStaging)?;
361 let receipt = self.store.activate(staged)?;
362 Ok(ActivatedArtifact {
363 descriptor: verified.descriptor,
364 receipt,
365 })
366 }
367
368 fn finish_activation(
369 &self,
370 activated: ActivatedArtifact,
371 faults: &dyn UpdateFaultInjector,
372 ) -> UpdateResult<UpdateOutcome> {
373 let health = self.health.ok_or_else(|| {
374 UpdateError::Health("activation health check is not configured".to_string())
375 })?;
376 let result = faults
377 .check(UpdateFaultPoint::AfterActivation)
378 .and_then(|_| health.check(&activated.descriptor))
379 .and_then(|_| faults.check(UpdateFaultPoint::BeforeCommit))
380 .and_then(|_| self.store.commit(&activated.receipt));
381 match result {
382 Ok(()) => Ok(UpdateOutcome::Applied(activated.descriptor)),
383 Err(cause) => self.rollback_after_failure(activated, cause),
384 }
385 }
386
387 fn rollback_after_failure(
388 &self,
389 activated: ActivatedArtifact,
390 cause: UpdateError,
391 ) -> UpdateResult<UpdateOutcome> {
392 match self.store.rollback(&activated.receipt) {
393 Ok(()) => Ok(UpdateOutcome::RolledBack {
394 attempted: activated.descriptor,
395 reason: cause.to_string(),
396 }),
397 Err(rollback) => Err(UpdateError::RollbackFailed {
398 cause: cause.to_string(),
399 rollback: rollback.to_string(),
400 }),
401 }
402 }
403}