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