1use super::*;
2use std::collections::{BTreeMap, BTreeSet};
3
4mod artifacts;
5mod canisters;
6mod config_digests;
7mod controllers;
8mod module_hashes;
9mod pools;
10mod receipt_resume;
11mod root_subnet;
12mod safety;
13mod verifier_readiness;
14
15use artifacts::compare_artifacts;
16#[cfg(test)]
17pub(super) use artifacts::{
18 ARTIFACT_DUPLICATE_DIFF_CATEGORY, ARTIFACT_FILE_DIGEST_MISMATCH_CODE,
19 ARTIFACT_FILE_SHA256_DIFF_CATEGORY, ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
20 DUPLICATE_ARTIFACT_OBSERVED_CODE, DUPLICATE_PLANNED_ARTIFACT_ROLE_CODE,
21 PLANNED_ARTIFACT_DUPLICATE_DIFF_CATEGORY, PLANNED_ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
22};
23pub(in crate::deployment_truth) use artifacts::{
24 ARTIFACT_MISSING_CODE, is_artifact_role_failure_code,
25};
26pub(super) use artifacts::{ARTIFACT_ROLE_CONFLICT_CODE, PLANNED_ARTIFACT_ROLE_CONFLICT_CODE};
27#[cfg(test)]
28pub(super) use canisters::{
29 CANISTER_DUPLICATE_DIFF_CATEGORY, CANISTER_EXTRA_DIFF_CATEGORY,
30 CANISTER_ID_ROLE_CONFLICT_DIFF_CATEGORY, CANISTER_ROLE_AMBIGUOUS_CODE,
31 CANISTER_ROLE_AMBIGUOUS_DIFF_CATEGORY, CANISTER_ROLE_MISMATCH_CODE, CANISTER_UNOBSERVED_CODE,
32 DUPLICATE_CANISTER_OBSERVED_CODE, DUPLICATE_PLANNED_CANISTER_ROLE_CODE,
33 EXTRA_CANISTER_OBSERVED_CODE, PLANNED_CANISTER_DUPLICATE_DIFF_CATEGORY,
34 PLANNED_CANISTER_ID_CONFLICT_DIFF_CATEGORY, PLANNED_CANISTER_ROLE_CONFLICT_DIFF_CATEGORY,
35 ROLE_MISMATCH_DIFF_CATEGORY, SUBNET_REGISTRY_ROLE_MISSING_CODE, UNSAFE_CONTROL_CLASS_CODE,
36};
37pub(super) use canisters::{
38 CANISTER_ID_ROLE_CONFLICT_CODE, PLANNED_CANISTER_ID_CONFLICT_CODE,
39 PLANNED_CANISTER_ROLE_CONFLICT_CODE,
40};
41use canisters::{compare_canisters, compare_observed_canister_id_conflicts};
42#[cfg(test)]
43pub(super) use config_digests::{RAW_CONFIG_DIGEST_MISMATCH_CODE, RAW_CONFIG_SHA256_DIFF_CATEGORY};
44use config_digests::{compare_embedded_config, compare_raw_config};
45use controllers::compare_authority_profile;
46#[cfg(test)]
47pub(super) use controllers::{
48 CONTROLLER_AUTHORITY_OVERLAP_CODE, CONTROLLER_EXTRA_DIFF_CATEGORY,
49 CONTROLLER_MISSING_DIFF_CATEGORY, CONTROLLERS_UNOBSERVED_CODE,
50 EXPECTED_CONTROLLER_MISSING_CODE, EXTRA_CONTROLLER_OBSERVED_CODE,
51};
52use module_hashes::compare_module_hashes;
53#[cfg(test)]
54pub(super) use module_hashes::{
55 INSTALLED_MODULE_HASH_AMBIGUOUS_CODE, INSTALLED_MODULE_HASH_AMBIGUOUS_DIFF_CATEGORY,
56 INSTALLED_MODULE_HASH_DIFF_CATEGORY, INSTALLED_MODULE_HASH_MISMATCH_CODE,
57};
58pub(super) use pools::{
59 CANISTER_POOL_ROLE_CONFLICT_CODE, PLANNED_POOL_CONFLICT_CODE, PLANNED_POOL_ID_CONFLICT_CODE,
60 POOL_CANISTER_ID_CONFLICT_CODE,
61};
62#[cfg(test)]
63pub(super) use pools::{
64 CANISTER_POOL_ROLE_CONFLICT_DIFF_CATEGORY, DUPLICATE_PLANNED_POOL_CODE,
65 DUPLICATE_POOL_CANISTER_OBSERVED_CODE, EXTRA_POOL_CANISTER_OBSERVED_CODE,
66 PLANNED_POOL_CONFLICT_DIFF_CATEGORY, PLANNED_POOL_DUPLICATE_DIFF_CATEGORY,
67 PLANNED_POOL_ID_CONFLICT_DIFF_CATEGORY, POOL_CANISTER_DIFF_CATEGORY,
68 POOL_CANISTER_DUPLICATE_DIFF_CATEGORY, POOL_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
69 POOL_CANISTER_ID_DIFF_CATEGORY, POOL_CANISTER_ID_MISMATCH_CODE, POOL_CANISTER_MISSING_CODE,
70 POOL_CONTROL_CLASS_DIFF_CATEGORY, POOL_EXTRA_DIFF_CATEGORY, UNSAFE_POOL_CONTROL_CLASS_CODE,
71};
72use pools::{compare_observed_canister_pool_role_conflicts, compare_pools};
73pub use receipt_resume::compare_plan_inventory_and_receipt;
74#[cfg(test)]
75pub(super) use receipt_resume::{
76 DUPLICATE_RECEIPT_PHASE_CODE, DUPLICATE_RECEIPT_ROLE_PHASE_CODE,
77 RECEIPT_EXECUTION_STATUS_MISMATCH_CODE, RECEIPT_PLAN_MISMATCH_CODE,
78 RECEIPT_POSTCONDITION_UNVERIFIED_CODE,
79};
80pub(super) use receipt_resume::{RECEIPT_PHASE_CONFLICT_CODE, RECEIPT_ROLE_PHASE_CONFLICT_CODE};
81#[cfg(test)]
82pub(super) use root_subnet::ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE;
83pub(super) use root_subnet::apply_root_auth_signer_subnet_check;
84#[cfg(test)]
85pub(super) use root_subnet::{
86 RootSubnetEvidence, RootSubnetEvidenceSource, apply_root_auth_signer_subnet_check_with_source,
87};
88pub use safety::safety_report_from_diff;
89pub(in crate::deployment_truth::report) use safety::{resume_safety_reasons, safety_status};
90use verifier_readiness::compare_verifier_readiness;
91#[cfg(test)]
92pub(super) use verifier_readiness::{
93 DUPLICATE_PLANNED_VERIFIER_ROLE_EPOCH_CODE, DUPLICATE_VERIFIER_ROLE_EPOCH_OBSERVED_CODE,
94 PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY,
95 PLANNED_VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_NOT_OBSERVED_LABEL,
96 VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_DIFF_CATEGORY,
97 VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_STALE_CODE,
98 VERIFIER_ROLE_EPOCH_UNOBSERVED_CODE,
99};
100pub(super) use verifier_readiness::{
101 PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE, VERIFIER_ROLE_EPOCH_CONFLICT_CODE,
102};
103
104pub(in crate::deployment_truth) const DEPLOYMENT_MANIFEST_MISMATCH_CODE: &str =
105 "deployment_manifest_mismatch";
106pub(in crate::deployment_truth) const OBSERVATION_GAP_CODE: &str = "observation_gap";
107pub(in crate::deployment_truth) const PLAN_ASSUMPTION_CODE: &str = "plan_assumption";
108const IDENTITY_UNOBSERVED_CODE: &str = "identity_unobserved";
109const ENVIRONMENT_MISMATCH_CODE: &str = "environment_mismatch";
110pub(in crate::deployment_truth) const FLEET_IDENTITY_MISMATCH_CODE: &str =
111 "fleet_identity_mismatch";
112const ROOT_TRUST_ANCHOR_MISMATCH_CODE: &str = "root_trust_anchor_mismatch";
113const DEPLOYMENT_MANIFEST_UNOBSERVED_CODE: &str = "deployment_manifest_unobserved";
114
115#[must_use]
116pub fn is_evidence_conflict_finding_code(code: &str) -> bool {
117 matches!(
118 code,
119 PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
120 | ARTIFACT_ROLE_CONFLICT_CODE
121 | CANISTER_ID_ROLE_CONFLICT_CODE
122 | PLANNED_CANISTER_ROLE_CONFLICT_CODE
123 | PLANNED_CANISTER_ID_CONFLICT_CODE
124 | CANISTER_POOL_ROLE_CONFLICT_CODE
125 | PLANNED_POOL_CONFLICT_CODE
126 | PLANNED_POOL_ID_CONFLICT_CODE
127 | POOL_CANISTER_ID_CONFLICT_CODE
128 | RECEIPT_PHASE_CONFLICT_CODE
129 | RECEIPT_ROLE_PHASE_CONFLICT_CODE
130 | PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE
131 | VERIFIER_ROLE_EPOCH_CONFLICT_CODE
132 )
133}
134
135struct DuplicateEvidenceGroup {
139 subject: String,
140 count: usize,
141 evidence_label: String,
142 is_conflict: bool,
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct LocalDeploymentCheckRequest {
150 pub fleet_name: String,
151 pub app: String,
152 pub environment: String,
153 pub artifact_environment: String,
154 pub workspace_root: std::path::PathBuf,
155 pub icp_root: std::path::PathBuf,
156 pub config_path: Option<std::path::PathBuf>,
157 pub observed_at: String,
158 pub runtime_variant: String,
159 pub build_profile: String,
160}
161
162pub fn check_local_deployment(
164 request: &LocalDeploymentCheckRequest,
165) -> Result<DeploymentCheckV1, DeploymentTruthError> {
166 let plan = build_local_deployment_plan(&LocalDeploymentPlanRequest {
167 fleet_name: request.fleet_name.clone(),
168 app: request.app.clone(),
169 environment: request.environment.clone(),
170 artifact_environment: request.artifact_environment.clone(),
171 workspace_root: request.workspace_root.clone(),
172 icp_root: request.icp_root.clone(),
173 config_path: request.config_path.clone(),
174 runtime_variant: request.runtime_variant.clone(),
175 build_profile: request.build_profile.clone(),
176 });
177 let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
178 fleet_name: request.fleet_name.clone(),
179 environment: request.environment.clone(),
180 artifact_environment: request.artifact_environment.clone(),
181 workspace_root: request.workspace_root.clone(),
182 icp_root: request.icp_root.clone(),
183 config_path: request.config_path.clone(),
184 observed_at: request.observed_at.clone(),
185 })?;
186 let mut diff = compare_plan_to_inventory(&plan, &inventory);
187 apply_root_auth_signer_subnet_check(
188 &mut diff,
189 &inventory,
190 &request.environment,
191 &request.icp_root,
192 );
193 let report = safety_report_from_diff(
194 format!(
195 "local:{}:{}:report",
196 request.environment, request.fleet_name
197 ),
198 Some(format!(
199 "local:{}:{}:diff",
200 request.environment, request.fleet_name
201 )),
202 &diff,
203 );
204
205 Ok(DeploymentCheckV1 {
206 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
207 check_id: format!("local:{}:{}:check", request.environment, request.fleet_name),
208 plan,
209 inventory,
210 diff,
211 report,
212 })
213}
214
215fn refresh_resume_safety(diff: &mut DeploymentDiffV1) {
216 diff.resume_safety.status = safety_status(&diff.hard_failures, &diff.warnings);
217 diff.resume_safety.reasons = resume_safety_reasons(&diff.hard_failures, &diff.warnings);
218}
219
220#[must_use]
222pub fn compare_plan_to_inventory(
223 plan: &DeploymentPlanV1,
224 inventory: &DeploymentInventoryV1,
225) -> DeploymentDiffV1 {
226 let mut artifact_diff = Vec::new();
227 let mut controller_diff = Vec::new();
228 let mut pool_diff = Vec::new();
229 let mut embedded_config_diff = Vec::new();
230 let mut module_hash_diff = Vec::new();
231 let mut verifier_readiness_diff = Vec::new();
232 let mut hard_failures = Vec::new();
233 let mut warnings = Vec::new();
234
235 compare_identity(plan, inventory, &mut hard_failures);
236 compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
237 compare_artifacts(
238 plan,
239 inventory,
240 &mut artifact_diff,
241 &mut hard_failures,
242 &mut warnings,
243 );
244 compare_observed_canister_id_conflicts(
245 inventory,
246 &mut controller_diff,
247 &mut hard_failures,
248 &mut warnings,
249 );
250 compare_observed_canister_pool_role_conflicts(inventory, &mut pool_diff, &mut hard_failures);
251 compare_canisters(
252 plan,
253 inventory,
254 &mut controller_diff,
255 &mut hard_failures,
256 &mut warnings,
257 );
258 compare_pools(
259 plan,
260 inventory,
261 &mut pool_diff,
262 &mut hard_failures,
263 &mut warnings,
264 );
265 compare_module_hashes(
266 plan,
267 inventory,
268 &mut module_hash_diff,
269 &mut hard_failures,
270 &mut warnings,
271 );
272 compare_raw_config(
273 plan,
274 inventory,
275 &mut embedded_config_diff,
276 &mut hard_failures,
277 );
278 compare_embedded_config(
279 plan,
280 inventory,
281 &mut embedded_config_diff,
282 &mut hard_failures,
283 &mut warnings,
284 );
285 compare_verifier_readiness(
286 plan,
287 inventory,
288 &mut verifier_readiness_diff,
289 &mut hard_failures,
290 &mut warnings,
291 );
292 record_plan_assumptions(plan, &mut warnings);
293 for gap in &inventory.unresolved_observations {
294 warnings.push(SafetyFindingV1 {
295 code: OBSERVATION_GAP_CODE.to_string(),
296 message: gap.description.clone(),
297 severity: SafetySeverityV1::Warning,
298 subject: Some(gap.key.clone()),
299 });
300 }
301
302 let status = safety_status(&hard_failures, &warnings);
303 DeploymentDiffV1 {
304 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
305 plan_identity: plan.deployment_identity.clone(),
306 observed_identity: inventory.observed_identity.clone(),
307 artifact_diff,
308 controller_diff,
309 pool_diff,
310 embedded_config_diff,
311 module_hash_diff,
312 verifier_readiness_diff,
313 resume_safety: ResumeSafetyV1 {
314 status,
315 reasons: resume_safety_reasons(&hard_failures, &warnings),
316 },
317 hard_failures,
318 warnings,
319 resumable_phases: Vec::new(),
320 }
321}
322
323fn record_plan_assumptions(plan: &DeploymentPlanV1, warnings: &mut Vec<SafetyFindingV1>) {
324 for assumption in &plan.unresolved_assumptions {
325 warnings.push(SafetyFindingV1 {
326 code: PLAN_ASSUMPTION_CODE.to_string(),
327 message: assumption.description.clone(),
328 severity: SafetySeverityV1::Warning,
329 subject: Some(assumption.key.clone()),
330 });
331 }
332}
333
334fn compare_identity(
335 plan: &DeploymentPlanV1,
336 inventory: &DeploymentInventoryV1,
337 hard_failures: &mut Vec<SafetyFindingV1>,
338) {
339 let Some(observed) = &inventory.observed_identity else {
340 hard_failures.push(finding(
341 IDENTITY_UNOBSERVED_CODE,
342 "deployment identity was not observed",
343 SafetySeverityV1::HardFailure,
344 None,
345 ));
346 return;
347 };
348
349 if observed.environment != plan.deployment_identity.environment {
350 hard_failures.push(finding(
351 ENVIRONMENT_MISMATCH_CODE,
352 format!(
353 "plan environment {} differs from observed environment {}",
354 plan.deployment_identity.environment, observed.environment
355 ),
356 SafetySeverityV1::HardFailure,
357 Some("deployment_identity.environment".to_string()),
358 ));
359 }
360 for (field, expected, actual) in [
361 (
362 "fleet_name",
363 Some(plan.deployment_identity.fleet_name.clone()),
364 Some(observed.fleet_name.clone()),
365 ),
366 (
367 "app",
368 Some(plan.deployment_identity.app.clone()),
369 Some(observed.app.clone()),
370 ),
371 (
372 "canonical_network_id",
373 plan.deployment_identity
374 .canonical_network_id
375 .map(|value| value.to_string()),
376 observed.canonical_network_id.map(|value| value.to_string()),
377 ),
378 (
379 "fleet_id",
380 plan.deployment_identity
381 .fleet_id
382 .map(|value| value.to_string()),
383 observed.fleet_id.map(|value| value.to_string()),
384 ),
385 ] {
386 match (expected, actual) {
387 (Some(expected), Some(actual)) if expected != actual => {
388 hard_failures.push(finding(
389 FLEET_IDENTITY_MISMATCH_CODE,
390 format!("plan {field} {expected} differs from observed {field} {actual}"),
391 SafetySeverityV1::HardFailure,
392 Some(format!("deployment_identity.{field}")),
393 ));
394 }
395 (Some(expected), None) => {
396 hard_failures.push(finding(
397 FLEET_IDENTITY_MISMATCH_CODE,
398 format!("plan {field} {expected} was not observed"),
399 SafetySeverityV1::HardFailure,
400 Some(format!("deployment_identity.{field}")),
401 ));
402 }
403 _ => {}
404 }
405 }
406 if let (Some(expected), Some(actual)) = (
407 plan.deployment_identity.root_principal.as_ref(),
408 observed.root_principal.as_ref(),
409 ) && expected != actual
410 {
411 hard_failures.push(finding(
412 ROOT_TRUST_ANCHOR_MISMATCH_CODE,
413 format!("plan root {expected} differs from observed root {actual}"),
414 SafetySeverityV1::HardFailure,
415 Some("deployment_identity.root_principal".to_string()),
416 ));
417 }
418 match (
419 plan.deployment_identity.deployment_manifest_digest.as_ref(),
420 observed.deployment_manifest_digest.as_ref(),
421 ) {
422 (Some(expected), Some(actual)) if expected != actual => {
423 hard_failures.push(finding(
424 DEPLOYMENT_MANIFEST_MISMATCH_CODE,
425 "deployment manifest digest differs from the observed local config",
426 SafetySeverityV1::HardFailure,
427 Some("deployment_identity.deployment_manifest_digest".to_string()),
428 ));
429 }
430 (Some(_), None) => {
431 hard_failures.push(finding(
432 DEPLOYMENT_MANIFEST_UNOBSERVED_CODE,
433 "deployment manifest digest was not observed",
434 SafetySeverityV1::HardFailure,
435 Some("deployment_identity.deployment_manifest_digest".to_string()),
436 ));
437 }
438 _ => {}
439 }
440}
441
442fn finding(
443 code: impl Into<String>,
444 message: impl Into<String>,
445 severity: SafetySeverityV1,
446 subject: Option<String>,
447) -> SafetyFindingV1 {
448 SafetyFindingV1 {
449 code: code.into(),
450 message: message.into(),
451 severity,
452 subject,
453 }
454}
455
456fn diff_item(
457 category: impl Into<String>,
458 subject: impl Into<String>,
459 expected: Option<String>,
460 observed: Option<String>,
461 severity: SafetySeverityV1,
462) -> DiffItemV1 {
463 DiffItemV1 {
464 category: category.into(),
465 subject: subject.into(),
466 expected,
467 observed,
468 severity,
469 }
470}
471
472fn duplicate_evidence_groups<T>(
473 items: &[T],
474 subject: impl Fn(&T) -> String,
475 evidence: impl Fn(&T) -> String,
476 evidence_separator: &str,
477) -> Vec<DuplicateEvidenceGroup> {
478 let mut groups = Vec::new();
479 for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
480 if entries.len() <= 1 {
481 continue;
482 }
483 let evidence_values = entries
484 .iter()
485 .map(|entry| evidence(entry))
486 .collect::<BTreeSet<_>>();
487 groups.push(DuplicateEvidenceGroup {
488 subject,
489 count: entries.len(),
490 evidence_label: evidence_values
491 .iter()
492 .cloned()
493 .collect::<Vec<_>>()
494 .join(evidence_separator),
495 is_conflict: evidence_values.len() > 1,
496 });
497 }
498 groups
499}
500
501fn duplicate_evidence_groups_by<T, K>(
502 items: &[T],
503 subject: impl Fn(&T) -> String,
504 evidence_key: impl Fn(&T) -> K,
505 evidence_label: impl Fn(&T) -> String,
506 evidence_separator: &str,
507) -> Vec<DuplicateEvidenceGroup>
508where
509 K: Ord,
510{
511 let mut groups = Vec::new();
512 for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
513 if entries.len() <= 1 {
514 continue;
515 }
516 let evidence_values = entries
517 .iter()
518 .map(|entry| (evidence_key(entry), evidence_label(entry)))
519 .collect::<BTreeMap<_, _>>();
520 groups.push(DuplicateEvidenceGroup {
521 subject,
522 count: entries.len(),
523 evidence_label: evidence_values
524 .values()
525 .cloned()
526 .collect::<Vec<_>>()
527 .join(evidence_separator),
528 is_conflict: evidence_values.len() > 1,
529 });
530 }
531 groups
532}
533
534fn conflicting_assignment_groups<T>(
535 items: &[T],
536 subject: impl Fn(&T) -> Option<String>,
537 value: impl Fn(&T) -> String,
538 value_separator: &str,
539) -> Vec<DuplicateEvidenceGroup> {
540 let mut groups = Vec::new();
541 for (subject, entries) in group_by_subject(items, subject) {
542 if entries.len() <= 1 {
543 continue;
544 }
545 let values = entries
546 .iter()
547 .map(|entry| value(entry))
548 .collect::<BTreeSet<_>>();
549 if values.len() <= 1 {
550 continue;
551 }
552 groups.push(DuplicateEvidenceGroup {
553 subject,
554 count: entries.len(),
555 evidence_label: values
556 .iter()
557 .cloned()
558 .collect::<Vec<_>>()
559 .join(value_separator),
560 is_conflict: true,
561 });
562 }
563 groups
564}
565
566fn group_by_subject<T>(
567 items: &[T],
568 subject: impl Fn(&T) -> Option<String>,
569) -> BTreeMap<String, Vec<&T>> {
570 let mut by_subject = BTreeMap::<String, Vec<&T>>::new();
571 for item in items {
572 if let Some(subject) = subject(item) {
573 by_subject.entry(subject).or_default().push(item);
574 }
575 }
576 by_subject
577}
578
579#[cfg(test)]
580mod classification_tests {
581 use super::*;
582
583 #[test]
584 fn finding_classification_uses_exact_owner_codes() {
585 assert!(is_evidence_conflict_finding_code(
586 ARTIFACT_ROLE_CONFLICT_CODE
587 ));
588 assert!(is_evidence_conflict_finding_code(
589 RECEIPT_PHASE_CONFLICT_CODE
590 ));
591 assert!(!is_evidence_conflict_finding_code("artifact_conflict"));
592 assert!(!is_evidence_conflict_finding_code("conflict"));
593
594 assert!(is_artifact_role_failure_code(ARTIFACT_MISSING_CODE));
595 assert!(!is_artifact_role_failure_code(
596 PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
597 ));
598 }
599}