Skip to main content

canic_host/deployment_truth/
executor.rs

1use 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
9///
10/// DeploymentExecutor
11///
12pub trait DeploymentExecutor {
13    fn execution_context(&self) -> DeploymentExecutionContextV1;
14}
15
16///
17/// CurrentCliDeploymentExecutor
18///
19#[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}