1use riffdb_types::{ApplicationRoleHash, CapabilityId};
2
3use crate::{
4 ApplicationInstallationPlan, CredentialDestination, InstallationArtifact, InstallationFeature,
5 InstallationPlanError, InstallationPlanErrorKind, InstallationRole, InstallationSymbol,
6 InstallationTarget, QuerySecretOutput, RoleAuthorityAtom, RoleAuthoritySet, RoleOperation,
7};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum InstallationLifecycleState {
12 Ready,
14 NotReady,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct ObservedInstallationRole {
21 name: InstallationSymbol,
22 role_hash: ApplicationRoleHash,
23 authority: RoleAuthoritySet,
24}
25
26impl ObservedInstallationRole {
27 pub fn new(
29 name: InstallationSymbol,
30 role_hash: ApplicationRoleHash,
31 operations: Vec<RoleOperation>,
32 ) -> Result<Self, InstallationPlanError> {
33 Self::new_with_authority(name, role_hash, RoleAuthoritySet::new(operations, vec![])?)
34 }
35
36 pub fn new_with_authority(
38 name: InstallationSymbol,
39 role_hash: ApplicationRoleHash,
40 authority: RoleAuthoritySet,
41 ) -> Result<Self, InstallationPlanError> {
42 Ok(Self {
43 name,
44 role_hash,
45 authority,
46 })
47 }
48
49 #[must_use]
51 pub const fn name(&self) -> &InstallationSymbol {
52 &self.name
53 }
54
55 #[must_use]
57 pub const fn role_hash(&self) -> ApplicationRoleHash {
58 self.role_hash
59 }
60
61 #[must_use]
63 pub fn operations(&self) -> &[RoleOperation] {
64 self.authority.operations()
65 }
66
67 #[must_use]
69 pub fn secret_outputs(&self) -> &[QuerySecretOutput] {
70 self.authority.secret_outputs()
71 }
72
73 #[must_use]
75 pub const fn authority(&self) -> &RoleAuthoritySet {
76 &self.authority
77 }
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct ObservedCredentialDestination {
83 name: InstallationSymbol,
84 capability_id: CapabilityId,
85}
86
87impl ObservedCredentialDestination {
88 #[must_use]
90 pub const fn new(name: InstallationSymbol, capability_id: CapabilityId) -> Self {
91 Self {
92 name,
93 capability_id,
94 }
95 }
96
97 #[must_use]
99 pub const fn name(&self) -> &InstallationSymbol {
100 &self.name
101 }
102
103 #[must_use]
105 pub const fn capability_id(&self) -> CapabilityId {
106 self.capability_id
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct InstallationRemoteState {
113 target: InstallationTarget,
114 lifecycle: InstallationLifecycleState,
115 contract: Option<crate::InstallationContract>,
116 artifacts: Vec<InstallationArtifact>,
117 roles: Vec<ObservedInstallationRole>,
118 credentials: Vec<ObservedCredentialDestination>,
119 supported_features: Vec<InstallationFeature>,
120}
121
122impl InstallationRemoteState {
123 pub fn new(
125 target: InstallationTarget,
126 lifecycle: InstallationLifecycleState,
127 contract: Option<crate::InstallationContract>,
128 mut artifacts: Vec<InstallationArtifact>,
129 mut roles: Vec<ObservedInstallationRole>,
130 mut credentials: Vec<ObservedCredentialDestination>,
131 mut supported_features: Vec<InstallationFeature>,
132 ) -> Result<Self, InstallationPlanError> {
133 if artifacts.len() > crate::MAX_INSTALLATION_ARTIFACTS
134 || roles.len() > crate::MAX_INSTALLATION_ROLES
135 || credentials.len() > crate::MAX_CREDENTIAL_DESTINATIONS
136 {
137 return Err(InstallationPlanError::from_kind_for_diff(
138 InstallationPlanErrorKind::LimitExceeded,
139 ));
140 }
141 artifacts.sort();
142 reject_duplicate_keys(
143 artifacts
144 .windows(2)
145 .any(|pair| (pair[0].kind(), pair[0].name()) == (pair[1].kind(), pair[1].name())),
146 )?;
147 roles.sort_by(|left, right| left.name.cmp(&right.name));
148 reject_duplicate_keys(roles.windows(2).any(|pair| pair[0].name == pair[1].name))?;
149 credentials.sort_by(|left, right| left.name.cmp(&right.name));
150 reject_duplicate_keys(
151 credentials
152 .windows(2)
153 .any(|pair| pair[0].name == pair[1].name),
154 )?;
155 supported_features.sort();
156 reject_duplicate_keys(supported_features.windows(2).any(|pair| pair[0] == pair[1]))?;
157 Ok(Self {
158 target,
159 lifecycle,
160 contract,
161 artifacts,
162 roles,
163 credentials,
164 supported_features,
165 })
166 }
167
168 #[must_use]
170 pub const fn target(&self) -> &InstallationTarget {
171 &self.target
172 }
173
174 #[must_use]
176 pub const fn lifecycle(&self) -> InstallationLifecycleState {
177 self.lifecycle
178 }
179}
180
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum InstallationContractAction {
184 AlreadyExact,
186 DeployGenesis,
188 ApplyMigration,
190 StopForMigration,
192}
193
194#[derive(Clone, Debug, Eq, PartialEq)]
196pub enum InstallationArtifactAction {
197 AlreadyExact(InstallationArtifact),
199 Publish(InstallationArtifact),
201 IdentityConflict {
203 desired: InstallationArtifact,
205 observed: InstallationArtifact,
207 },
208}
209
210#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct InstallationRoleDiff {
213 name: InstallationSymbol,
214 current_hash: Option<ApplicationRoleHash>,
215 desired_hash: ApplicationRoleHash,
216 additions: Vec<RoleAuthorityAtom>,
217 removals: Vec<RoleAuthorityAtom>,
218 widening_approved: bool,
219}
220
221impl InstallationRoleDiff {
222 #[must_use]
224 pub const fn name(&self) -> &InstallationSymbol {
225 &self.name
226 }
227
228 #[must_use]
230 pub const fn current_hash(&self) -> Option<ApplicationRoleHash> {
231 self.current_hash
232 }
233
234 #[must_use]
236 pub const fn desired_hash(&self) -> ApplicationRoleHash {
237 self.desired_hash
238 }
239
240 #[must_use]
242 pub fn additions(&self) -> &[RoleAuthorityAtom] {
243 &self.additions
244 }
245
246 #[must_use]
248 pub fn removals(&self) -> &[RoleAuthorityAtom] {
249 &self.removals
250 }
251
252 #[must_use]
254 pub const fn widening_approved(&self) -> bool {
255 self.widening_approved
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
261pub enum InstallationCredentialAction {
262 AlreadyExact(InstallationSymbol),
264 Create(InstallationSymbol),
266 Rotate(InstallationSymbol),
268 Occupied(InstallationSymbol),
270}
271
272#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct ApplicationInstallationDiff {
275 lifecycle_ready: bool,
276 contract: InstallationContractAction,
277 artifacts: Vec<InstallationArtifactAction>,
278 roles: Vec<InstallationRoleDiff>,
279 credentials: Vec<InstallationCredentialAction>,
280 missing_features: Vec<InstallationFeature>,
281}
282
283impl ApplicationInstallationDiff {
284 pub fn compile(
286 plan: &ApplicationInstallationPlan,
287 remote: &InstallationRemoteState,
288 ) -> Result<Self, InstallationPlanError> {
289 let input = plan.input();
290 if &input.target != remote.target() {
291 return Err(InstallationPlanError::from_kind_for_diff(
292 InstallationPlanErrorKind::IdentityMismatch,
293 ));
294 }
295 let contract = match remote.contract {
296 None => InstallationContractAction::DeployGenesis,
297 Some(active) if active == input.contract => InstallationContractAction::AlreadyExact,
298 Some(active)
299 if input.migration.is_some_and(|migration| {
300 migration.parent_version() == active.version()
301 && migration.parent_bundle_hash() == active.bundle_hash()
302 }) =>
303 {
304 InstallationContractAction::ApplyMigration
305 }
306 Some(_) => InstallationContractAction::StopForMigration,
307 };
308 let artifacts = input
309 .artifacts
310 .iter()
311 .map(|desired| {
312 remote
313 .artifacts
314 .iter()
315 .find(|observed| {
316 observed.kind() == desired.kind() && observed.name() == desired.name()
317 })
318 .map_or_else(
319 || InstallationArtifactAction::Publish(desired.clone()),
320 |observed| {
321 if observed == desired {
322 InstallationArtifactAction::AlreadyExact(desired.clone())
323 } else {
324 InstallationArtifactAction::IdentityConflict {
325 desired: desired.clone(),
326 observed: observed.clone(),
327 }
328 }
329 },
330 )
331 })
332 .collect();
333 let roles = input
334 .roles
335 .iter()
336 .map(|desired| role_diff(desired, &remote.roles))
337 .collect();
338 let credentials = input
339 .credential_destinations
340 .iter()
341 .map(|desired| credential_diff(desired, &remote.credentials))
342 .collect();
343 let missing_features = input
344 .required_features
345 .iter()
346 .filter(|feature| remote.supported_features.binary_search(feature).is_err())
347 .copied()
348 .collect();
349 Ok(Self {
350 lifecycle_ready: remote.lifecycle == InstallationLifecycleState::Ready,
351 contract,
352 artifacts,
353 roles,
354 credentials,
355 missing_features,
356 })
357 }
358
359 #[must_use]
361 pub const fn lifecycle_ready(&self) -> bool {
362 self.lifecycle_ready
363 }
364
365 #[must_use]
367 pub const fn contract(&self) -> InstallationContractAction {
368 self.contract
369 }
370
371 #[must_use]
373 pub fn artifacts(&self) -> &[InstallationArtifactAction] {
374 &self.artifacts
375 }
376
377 #[must_use]
379 pub fn roles(&self) -> &[InstallationRoleDiff] {
380 &self.roles
381 }
382
383 #[must_use]
385 pub fn credentials(&self) -> &[InstallationCredentialAction] {
386 &self.credentials
387 }
388
389 #[must_use]
391 pub fn missing_features(&self) -> &[InstallationFeature] {
392 &self.missing_features
393 }
394
395 #[must_use]
397 pub fn executable(&self) -> bool {
398 self.lifecycle_ready
399 && self.contract != InstallationContractAction::StopForMigration
400 && self.missing_features.is_empty()
401 && self.roles.iter().all(|role| {
402 role.additions.is_empty() || role.current_hash.is_none() || role.widening_approved
403 })
404 && self
405 .credentials
406 .iter()
407 .all(|action| !matches!(action, InstallationCredentialAction::Occupied(_)))
408 && self.artifacts.iter().all(|action| {
409 !matches!(action, InstallationArtifactAction::IdentityConflict { .. })
410 })
411 }
412}
413
414fn role_diff(
415 desired: &InstallationRole,
416 observed_roles: &[ObservedInstallationRole],
417) -> InstallationRoleDiff {
418 let observed = observed_roles
419 .iter()
420 .find(|observed| observed.name == *desired.name());
421 let existing = observed.map_or_else(Vec::new, |role| role.authority.atoms());
422 let desired_atoms = desired.desired_authority().atoms();
423 let additions = desired_atoms
424 .iter()
425 .filter(|atom| existing.binary_search(atom).is_err())
426 .cloned()
427 .collect::<Vec<_>>();
428 let removals = existing
429 .iter()
430 .filter(|atom| desired_atoms.binary_search(atom).is_err())
431 .cloned()
432 .collect::<Vec<_>>();
433 let widening_approved = additions.is_empty()
434 || observed.is_none()
435 || desired.widening_approval().is_some_and(|approval| {
436 observed.is_some_and(|role| approval.expected_previous_role() == role.role_hash)
437 && approval.additions() == additions
438 });
439 InstallationRoleDiff {
440 name: desired.name().clone(),
441 current_hash: observed.map(ObservedInstallationRole::role_hash),
442 desired_hash: desired.role_hash(),
443 additions,
444 removals,
445 widening_approved,
446 }
447}
448
449fn credential_diff(
450 desired: &CredentialDestination,
451 observed_credentials: &[ObservedCredentialDestination],
452) -> InstallationCredentialAction {
453 let observed = observed_credentials
454 .iter()
455 .find(|observed| observed.name == *desired.name());
456 match observed {
457 Some(observed) if observed.capability_id == desired.successor() => {
458 InstallationCredentialAction::AlreadyExact(desired.name().clone())
459 }
460 None if desired.expected_current().is_none() => {
461 InstallationCredentialAction::Create(desired.name().clone())
462 }
463 Some(observed) if Some(observed.capability_id) == desired.expected_current() => {
464 InstallationCredentialAction::Rotate(desired.name().clone())
465 }
466 _ => InstallationCredentialAction::Occupied(desired.name().clone()),
467 }
468}
469
470fn reject_duplicate_keys(duplicate: bool) -> Result<(), InstallationPlanError> {
471 if duplicate {
472 Err(InstallationPlanError::from_kind_for_diff(
473 InstallationPlanErrorKind::Duplicate,
474 ))
475 } else {
476 Ok(())
477 }
478}