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