services_hardening_diagnostics.rs (5628B)
1 #![forbid(unsafe_code)] 2 3 use std::process::Command; 4 5 use myc::{ 6 MYC_DIAGNOSTICS_CONTRACT_VERSION, MYC_LOG_RECORD_MAX_UTF8_BYTES, MycLogEvent, MycLogLevel, 7 MycLogRecord, MycProcessResult, MycServicePhase, 8 }; 9 10 const CONTRACT: &str = include_str!("../contracts/services_hardening/diagnostics.v1.json"); 11 const OPERATOR_CONTRACT: &str = 12 include_str!("../contracts/services_hardening/operator_contract.v1.json"); 13 14 fn process_results() -> [MycProcessResult; 7] { 15 [ 16 MycProcessResult::Success, 17 MycProcessResult::UnexpectedInternal, 18 MycProcessResult::InputOrConfiguration, 19 MycProcessResult::ServiceOrDependencyUnavailable, 20 MycProcessResult::StateOrIdentityUnavailable, 21 MycProcessResult::OperationRejectedOrConflict, 22 MycProcessResult::DoctorRequiredCheckFailed, 23 ] 24 } 25 26 #[test] 27 fn machine_contract_exit_inventory_matches_the_operator_contract_exactly() { 28 let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract"); 29 let operator: serde_json::Value = 30 serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract"); 31 assert_eq!(contract["schema"], "radroots.myc.diagnostics.v1"); 32 assert_eq!( 33 contract["contract_version"], 34 MYC_DIAGNOSTICS_CONTRACT_VERSION 35 ); 36 assert_eq!(contract["step"], 156); 37 assert_eq!( 38 contract["log_record"]["maximum_utf8_bytes_excluding_newline"], 39 MYC_LOG_RECORD_MAX_UTF8_BYTES 40 ); 41 42 let diagnostics = contract["exit_codes"].as_array().expect("exit codes"); 43 let operator = operator["exit_codes"].as_array().expect("operator exits"); 44 assert_eq!(diagnostics.len(), operator.len()); 45 for ((result, diagnostic), operator) in 46 process_results().into_iter().zip(diagnostics).zip(operator) 47 { 48 assert_eq!(diagnostic["code"], result.exit_code_u8()); 49 assert_eq!(diagnostic["name"], result.code()); 50 assert_eq!(diagnostic["code"], operator["code"]); 51 assert_eq!(diagnostic["name"], operator["name"]); 52 } 53 assert_eq!(contract["public_error_policy"]["error_source"], "none"); 54 assert_eq!(contract["stream_policy"]["logs_and_diagnostics"], "stderr"); 55 assert_eq!(contract["stream_policy"]["file_logging"], false); 56 } 57 58 #[test] 59 fn process_lifecycle_task_and_signal_records_are_exact_and_bounded() { 60 for result in process_results() { 61 let record = MycLogRecord::process_result(result); 62 let rendered = record.to_string(); 63 assert_eq!(record.event(), MycLogEvent::ProcessResult); 64 assert_eq!(record.code(), result.code()); 65 assert_eq!(record.process_exit(), Some(result)); 66 assert!(rendered.len() <= MYC_LOG_RECORD_MAX_UTF8_BYTES); 67 assert_eq!( 68 serde_json::from_str::<serde_json::Value>(&rendered).expect("record")["exit_code"], 69 result.exit_code_u8() 70 ); 71 } 72 73 for (phase, level, code) in [ 74 (MycServicePhase::Starting, MycLogLevel::Info, "starting"), 75 (MycServicePhase::Ready, MycLogLevel::Info, "ready"), 76 (MycServicePhase::Degraded, MycLogLevel::Warn, "degraded"), 77 (MycServicePhase::Unready, MycLogLevel::Warn, "unready"), 78 (MycServicePhase::Stopping, MycLogLevel::Info, "stopping"), 79 (MycServicePhase::Failed, MycLogLevel::Error, "failed"), 80 ] { 81 let record = MycLogRecord::lifecycle(phase); 82 assert_eq!(record.level(), level); 83 assert_eq!(record.event(), MycLogEvent::Lifecycle); 84 assert_eq!(record.code(), code); 85 assert_eq!(record.process_exit(), None); 86 assert!(!record.to_string().contains("exit_code")); 87 } 88 89 for (record, event, code) in [ 90 ( 91 MycLogRecord::critical_task_failed(), 92 MycLogEvent::CriticalTaskFailed, 93 "critical_task_failed", 94 ), 95 ( 96 MycLogRecord::shutdown_requested(), 97 MycLogEvent::ShutdownRequested, 98 "first_signal", 99 ), 100 ( 101 MycLogRecord::shutdown_forced(), 102 MycLogEvent::ShutdownForced, 103 "second_signal", 104 ), 105 ] { 106 assert_eq!(record.event(), event); 107 assert_eq!(record.code(), code); 108 assert!(record.to_string().len() <= MYC_LOG_RECORD_MAX_UTF8_BYTES); 109 } 110 } 111 112 #[test] 113 fn binary_writes_only_fixed_json_diagnostics_to_stderr() { 114 let canary = "secret-canary-private-key-path-sql-relay-url"; 115 let invalid = Command::new(env!("CARGO_BIN_EXE_myc")) 116 .arg(format!("--credential={canary}")) 117 .output() 118 .expect("invalid invocation"); 119 assert_eq!(invalid.status.code(), Some(2)); 120 assert!(invalid.stdout.is_empty()); 121 let invalid_stderr = String::from_utf8(invalid.stderr).expect("invalid stderr"); 122 assert_eq!( 123 invalid_stderr, 124 format!( 125 "{}\n", 126 MycLogRecord::process_result(MycProcessResult::InputOrConfiguration) 127 ) 128 ); 129 assert!(!invalid_stderr.contains(canary)); 130 131 let repo_local = tempfile::tempdir().expect("repo-local root"); 132 let unconfigured = Command::new(env!("CARGO_BIN_EXE_myc")) 133 .args(["--profile", "repo-local", "--instance", "primary"]) 134 .arg("--repo-local-root") 135 .arg(repo_local.path()) 136 .arg("run") 137 .output() 138 .expect("admitted invocation"); 139 assert_eq!(unconfigured.status.code(), Some(2)); 140 assert!(unconfigured.stdout.is_empty()); 141 assert_eq!( 142 String::from_utf8(unconfigured.stderr).expect("unconfigured stderr"), 143 format!( 144 "{}\n", 145 MycLogRecord::process_result(MycProcessResult::InputOrConfiguration) 146 ) 147 ); 148 }