canic_host/deployment_truth/
executor.rs1use super::{
2 AuthorityReconciliationPlanV1, AuthorityReconciliationStateV1, DEPLOYMENT_TRUTH_SCHEMA_VERSION,
3 DeploymentExecutionContextV1, DeploymentExecutionPreflightStatusV1,
4 DeploymentExecutionPreflightV1, DeploymentExecutorBackendV1, DeploymentExecutorCapabilityV1,
5 DeploymentPlanV1, SafetyFindingV1, SafetyReportV1, SafetySeverityV1, SafetyStatusV1,
6};
7use std::collections::BTreeSet;
8
9pub trait DeploymentExecutor {
13 fn execution_context(&self) -> DeploymentExecutionContextV1;
14}
15
16#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct CurrentCliDeploymentExecutor {
21 context: DeploymentExecutionContextV1,
22}
23
24impl CurrentCliDeploymentExecutor {
25 #[must_use]
26 pub fn new(
27 workspace_root: Option<String>,
28 icp_root: Option<String>,
29 artifact_roots: Vec<String>,
30 ) -> Self {
31 Self {
32 context: current_cli_execution_context(workspace_root, icp_root, artifact_roots),
33 }
34 }
35}
36
37impl DeploymentExecutor for CurrentCliDeploymentExecutor {
38 fn execution_context(&self) -> DeploymentExecutionContextV1 {
39 self.context.clone()
40 }
41}
42
43pub const CURRENT_CLI_EXECUTOR_CAPABILITIES: &[DeploymentExecutorCapabilityV1] = &[
44 DeploymentExecutorCapabilityV1::CreateCanister,
45 DeploymentExecutorCapabilityV1::CanisterStatus,
46 DeploymentExecutorCapabilityV1::UpdateSettings,
47 DeploymentExecutorCapabilityV1::InstallCode,
48 DeploymentExecutorCapabilityV1::Call,
49 DeploymentExecutorCapabilityV1::Query,
50 DeploymentExecutorCapabilityV1::StageArtifact,
51];
52
53pub const CURRENT_INSTALL_EXECUTION_PHASES: &[&str] = &[
54 "create_root",
55 "build_artifacts",
56 "materialize_artifacts",
57 "install_root",
58 "stage_release_set",
59 "resume_bootstrap",
60 "wait_ready",
61 "post_validate",
62];
63
64#[must_use]
65pub fn current_cli_execution_context(
66 workspace_root: Option<String>,
67 icp_root: Option<String>,
68 artifact_roots: Vec<String>,
69) -> DeploymentExecutionContextV1 {
70 DeploymentExecutionContextV1 {
71 workspace_root,
72 icp_root,
73 artifact_roots,
74 backend: DeploymentExecutorBackendV1::CurrentCli,
75 backend_capabilities: CURRENT_CLI_EXECUTOR_CAPABILITIES.to_vec(),
76 }
77}
78
79#[must_use]
80pub fn missing_executor_capabilities(
81 available: &[DeploymentExecutorCapabilityV1],
82 required: &[DeploymentExecutorCapabilityV1],
83) -> Vec<DeploymentExecutorCapabilityV1> {
84 let available = available.iter().copied().collect::<BTreeSet<_>>();
85 required
86 .iter()
87 .copied()
88 .filter(|capability| !available.contains(capability))
89 .collect()
90}
91
92#[must_use]
93pub fn has_executor_capabilities(
94 available: &[DeploymentExecutorCapabilityV1],
95 required: &[DeploymentExecutorCapabilityV1],
96) -> bool {
97 missing_executor_capabilities(available, required).is_empty()
98}
99
100#[must_use]
101pub fn deployment_execution_preflight(
102 plan: &DeploymentPlanV1,
103 safety_report: &SafetyReportV1,
104 authority_plan: &AuthorityReconciliationPlanV1,
105 executor: &impl DeploymentExecutor,
106 required_capabilities: &[DeploymentExecutorCapabilityV1],
107) -> DeploymentExecutionPreflightV1 {
108 let execution_context = executor.execution_context();
109 let missing_capabilities = missing_executor_capabilities(
110 &execution_context.backend_capabilities,
111 required_capabilities,
112 );
113 let blockers =
114 deployment_execution_blockers(safety_report, authority_plan, &missing_capabilities);
115 let status = if blockers.is_empty() {
116 DeploymentExecutionPreflightStatusV1::Ready
117 } else {
118 DeploymentExecutionPreflightStatusV1::Blocked
119 };
120
121 DeploymentExecutionPreflightV1 {
122 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
123 plan_id: plan.plan_id.clone(),
124 safety_report_id: safety_report.report_id.clone(),
125 authority_plan_id: authority_plan.plan_id.clone(),
126 backend: execution_context.backend,
127 status,
128 planned_phases: CURRENT_INSTALL_EXECUTION_PHASES
129 .iter()
130 .map(|phase| (*phase).to_string())
131 .collect(),
132 required_capabilities: required_capabilities.to_vec(),
133 missing_capabilities,
134 blockers,
135 }
136}
137
138fn deployment_execution_blockers(
139 safety_report: &SafetyReportV1,
140 authority_plan: &AuthorityReconciliationPlanV1,
141 missing_capabilities: &[DeploymentExecutorCapabilityV1],
142) -> Vec<SafetyFindingV1> {
143 let mut blockers = Vec::new();
144
145 if matches!(safety_report.status, SafetyStatusV1::Blocked) {
146 blockers.push(SafetyFindingV1 {
147 code: "deployment_safety_blocked".to_string(),
148 message: safety_report.summary.clone(),
149 severity: SafetySeverityV1::HardFailure,
150 subject: Some(safety_report.report_id.clone()),
151 });
152 }
153 blockers.extend(safety_report.hard_failures.clone());
154 blockers.extend(
155 authority_plan
156 .hard_failures
157 .iter()
158 .filter(|failure| failure.code != "authority_unsafe_blocked")
159 .cloned(),
160 );
161
162 for action in &authority_plan.canister_actions {
163 match action.state {
164 AuthorityReconciliationStateV1::AlreadyCorrect => {}
165 AuthorityReconciliationStateV1::CanApplyAutomatically => {
166 blockers.push(SafetyFindingV1 {
167 code: "authority_controller_change_pending".to_string(),
168 message: action.reason.clone(),
169 severity: SafetySeverityV1::HardFailure,
170 subject: action
171 .canister_id
172 .clone()
173 .or_else(|| action.role.clone())
174 .or_else(|| Some("authority".to_string())),
175 });
176 }
177 AuthorityReconciliationStateV1::RequiresExternalAction => {
178 blockers.push(SafetyFindingV1 {
179 code: "authority_external_action_required".to_string(),
180 message: action.reason.clone(),
181 severity: SafetySeverityV1::HardFailure,
182 subject: action
183 .canister_id
184 .clone()
185 .or_else(|| action.role.clone())
186 .or_else(|| Some("authority".to_string())),
187 });
188 }
189 AuthorityReconciliationStateV1::UnsafeBlocked => {
190 blockers.push(SafetyFindingV1 {
191 code: "authority_unsafe_blocked".to_string(),
192 message: action.reason.clone(),
193 severity: SafetySeverityV1::HardFailure,
194 subject: action
195 .canister_id
196 .clone()
197 .or_else(|| action.role.clone())
198 .or_else(|| Some("authority".to_string())),
199 });
200 }
201 AuthorityReconciliationStateV1::Unknown => {
202 blockers.push(SafetyFindingV1 {
203 code: "authority_observation_missing".to_string(),
204 message: action.reason.clone(),
205 severity: SafetySeverityV1::HardFailure,
206 subject: action
207 .canister_id
208 .clone()
209 .or_else(|| action.role.clone())
210 .or_else(|| Some("authority".to_string())),
211 });
212 }
213 }
214 }
215
216 for capability in missing_capabilities {
217 blockers.push(SafetyFindingV1 {
218 code: "executor_capability_missing".to_string(),
219 message: format!("executor backend is missing required capability: {capability:?}"),
220 severity: SafetySeverityV1::HardFailure,
221 subject: Some(format!("{capability:?}")),
222 });
223 }
224
225 blockers
226}