canic_host/component_operation/ops/transport/
mod.rs1use crate::{
6 canister_protocol::{call_with_arg, query_with_arg},
7 component_operation::{
8 ComponentOperationError,
9 model::{ComponentAuthorityRecord, ComponentPlanRecord},
10 ops::ComponentTransport,
11 view::{ComponentObservation, ComponentProgressObservation},
12 },
13 fleet_ensure::{
14 ops::{EnsurePaths, read_state},
15 resolve_current_fleet,
16 },
17 icp::IcpCli,
18 protocol_binding::{ResolvedProtocolBinding, resolve_registry_protocol_binding},
19};
20use candid::{CandidType, Principal};
21use canic_control_plane::dto::{
22 fleet_coordinator::{CoordinatorRegistryRequest, CoordinatorRegistryResponse},
23 root::RootOperationStatusResponse,
24};
25use canic_core::{
26 diagnostics::codes::STATE_UNAVAILABLE,
27 dto::{
28 component_registry::RootComponentAllocationRequest,
29 fleet_registry::FleetSubnetRootStatus,
30 fleet_subnet_root::FleetSubnetRootAuthority,
31 pool::{CanisterPoolResponse, CanisterPoolStatusRequest},
32 role::{OperationReceipt, OperationStatusRequest},
33 },
34 ids::{ComponentSpecId, FleetSubnetRootBinding},
35 protocol,
36};
37use serde::Deserialize;
38use sha2::{Digest, Sha256};
39use std::path::{Path, PathBuf};
40
41#[derive(CandidType, Deserialize)]
42enum RootRead {
43 FleetAuthority,
44 Operation(OperationStatusRequest),
45 Pool(CanisterPoolStatusRequest),
46}
47
48#[derive(CandidType, Deserialize)]
49enum RootResponse {
50 FleetAuthority(Box<FleetSubnetRootAuthority>),
51 Operation(Box<RootOperationStatusResponse>),
52 Pool(Box<CanisterPoolResponse>),
53}
54
55#[derive(CandidType, Deserialize)]
56enum RootCommand {
57 ProvisionComponent(RootComponentAllocationRequest),
58}
59
60#[derive(CandidType, Deserialize)]
61enum RootCommandResponse {
62 OperationAccepted(OperationReceipt),
63}
64
65pub struct IcpComponentTransport {
67 root: PathBuf,
68 icp: IcpCli,
69}
70
71impl IcpComponentTransport {
72 #[must_use]
74 pub fn new(root: &Path, icp: IcpCli) -> Self {
75 Self {
76 root: root.to_path_buf(),
77 icp: icp.with_cwd(root),
78 }
79 }
80
81 pub fn authority(
83 &self,
84 environment: &str,
85 fleet: &str,
86 root_name: &str,
87 spec: &ComponentSpecId,
88 ) -> Result<ComponentAuthorityRecord, ComponentOperationError> {
89 if self.icp.environment() != Some(environment) {
90 return Err(ComponentOperationError::Authority {
91 field: "ICP environment",
92 });
93 }
94 let current = self.current_fleet(environment, fleet)?;
95 let registry = current.initial_active_registry(fleet)?;
96 let desired = current
97 .plan
98 .reviewed_desired
99 .as_deref()
100 .ok_or(ComponentOperationError::Integrity)?
101 .desired();
102 let configured = desired
103 .canisters
104 .iter()
105 .find(|entry| entry.name == root_name)
106 .ok_or(ComponentOperationError::Missing)?;
107 let state = read_state(&EnsurePaths::under(&self.root, environment, fleet), fleet)?;
108 let principal = state
109 .principals
110 .get(root_name)
111 .ok_or(ComponentOperationError::Missing)?;
112 let root = registry
113 .fleet_subnet_roots
114 .iter()
115 .find(|entry| entry.fleet_subnet_root.to_text() == *principal)
116 .ok_or(ComponentOperationError::Missing)?;
117 if root.status != FleetSubnetRootStatus::Active {
118 return Err(ComponentOperationError::Authority {
119 field: "active Root",
120 });
121 }
122 let selected = registry
123 .component_specs
124 .iter()
125 .find(|entry| entry.component_spec == *spec)
126 .ok_or(ComponentOperationError::Spec)?;
127 if !root.component_admissions.iter().any(|entry| {
128 entry.component_spec == *spec
129 && entry.spec_hash == selected.spec_hash
130 && entry.maximum_root_instances > 0
131 }) {
132 return Err(ComponentOperationError::Spec);
133 }
134 let entry = current
135 .registry
136 .entries
137 .iter()
138 .find(|entry| entry.pid == *principal)
139 .ok_or(ComponentOperationError::Missing)?;
140 let protocol = resolve_registry_protocol_binding(&self.root, environment, entry)?;
141 let mut controllers = configured.controllers.clone();
142 for name in &configured.controller_canisters {
143 controllers.push(
144 state
145 .principals
146 .get(name)
147 .ok_or(ComponentOperationError::Missing)?
148 .clone(),
149 );
150 }
151 controllers.sort();
152 controllers.dedup();
153 let operator = Principal::from_text(self.icp.identity_principal_text()?)
154 .map_err(|_| ComponentOperationError::Authority { field: "operator" })?;
155 if !controllers.contains(&operator.to_text()) {
156 return Err(ComponentOperationError::Authority {
157 field: "operator controller",
158 });
159 }
160 Ok(ComponentAuthorityRecord {
161 environment: environment.to_string(),
162 fleet: fleet.to_string(),
163 root_name: root_name.to_string(),
164 source_plan_sha256: current.plan.plan_sha256.clone(),
165 binding: FleetSubnetRootBinding {
166 authority: registry.authority.clone(),
167 placement_subnet: root.placement_subnet,
168 fleet_subnet_root: root.fleet_subnet_root,
169 component_admissions: root.component_admissions.clone(),
170 component_topology_digest: root.component_topology_digest,
171 limits: root.limits.clone(),
172 funding: root.funding.clone(),
173 },
174 release_set: root.active_release_set,
175 root_module_sha256: entry
176 .module_hash
177 .clone()
178 .ok_or(ComponentOperationError::Integrity)?,
179 root_candid_sha256: protocol.binding().candid_sha256,
180 root_controllers: controllers,
181 registry_sha256: Sha256::digest(serde_json::to_vec(registry)?).into(),
182 operator,
183 component_spec: spec.clone(),
184 spec_hash: selected.spec_hash,
185 role: selected.component_role.clone(),
186 })
187 }
188
189 fn current_fleet(
190 &self,
191 environment: &str,
192 fleet: &str,
193 ) -> Result<crate::fleet_ensure::CurrentFleetResolution, ComponentOperationError> {
194 let current = resolve_current_fleet(&self.root, environment, fleet)?;
195 if crate::fleet_ensure::policy::expected_plan_sha256(¤t.plan)
196 != current.plan.plan_sha256
197 {
198 return Err(ComponentOperationError::Integrity);
199 }
200 let registry = current.initial_active_registry(fleet)?;
201 let network =
202 crate::network::resolve_canonical_network_id_from_root(&self.root, environment)?;
203 if registry.authority.binding.fleet.fleet.canonical_network_id != network {
204 return Err(ComponentOperationError::Authority {
205 field: "selected network",
206 });
207 }
208 Ok(current)
209 }
210
211 fn binding(
212 &self,
213 plan: &ComponentPlanRecord,
214 principal: Principal,
215 ) -> Result<ResolvedProtocolBinding, ComponentOperationError> {
216 let current = resolve_current_fleet(
217 &self.root,
218 &plan.authority.environment,
219 &plan.authority.fleet,
220 )?;
221 let entry = current
222 .registry
223 .entries
224 .iter()
225 .find(|entry| entry.pid == principal.to_text())
226 .ok_or(ComponentOperationError::Missing)?;
227 Ok(resolve_registry_protocol_binding(
228 &self.root,
229 &plan.authority.environment,
230 entry,
231 )?)
232 }
233
234 fn query(
235 &self,
236 plan: &ComponentPlanRecord,
237 request: &RootRead,
238 ) -> Result<RootResponse, ComponentOperationError> {
239 let principal = plan.authority.binding.fleet_subnet_root;
240 Ok(query_with_arg(
241 &self.icp,
242 &self.binding(plan, principal)?,
243 principal,
244 match request {
245 RootRead::Operation(_) => protocol::CANIC_ROOT_OPERATION_STATUS,
246 _ => protocol::CANIC_ROOT_STATUS,
247 },
248 request,
249 )?)
250 }
251}
252
253impl ComponentTransport for IcpComponentTransport {
254 fn observe(
255 &mut self,
256 plan: &ComponentPlanRecord,
257 ) -> Result<ComponentObservation, ComponentOperationError> {
258 let expected = &plan.authority;
259 let authority = self.authority(
260 &expected.environment,
261 &expected.fleet,
262 &expected.root_name,
263 &expected.component_spec,
264 )?;
265 crate::component_operation::policy::validate_authority(expected, &authority)?;
266 let RootResponse::FleetAuthority(root) = self.query(plan, &RootRead::FleetAuthority)?
267 else {
268 return Err(ComponentOperationError::Progress);
269 };
270 let matching_binding = root.binding == authority.binding;
271 let matching_release = root.initial_release_set == authority.release_set;
272 let matching_module = canic_core::cdk::utils::hash::hex_bytes(root.expected_module_hash)
273 == authority.root_module_sha256;
274 if !(matching_binding && matching_release && matching_module) {
275 return Err(ComponentOperationError::Authority {
276 field: "live Root binding",
277 });
278 }
279 let coordinator = authority.binding.authority.binding.coordinator;
280 let CoordinatorRegistryResponse::Registry(registry) = query_with_arg(
281 &self.icp,
282 &self.binding(plan, coordinator)?,
283 coordinator,
284 protocol::CANIC_COORDINATOR_REGISTRY,
285 &CoordinatorRegistryRequest::Registry,
286 )?;
287 let registry_sha256: [u8; 32] = Sha256::digest(serde_json::to_vec(®istry)?).into();
288 if registry_sha256 != authority.registry_sha256 {
289 return Err(ComponentOperationError::Authority {
290 field: "live Registry",
291 });
292 }
293 let report = self
294 .icp
295 .canister_status_report(&authority.binding.fleet_subnet_root.to_text())?;
296 let mut controllers = report
297 .settings
298 .ok_or(ComponentOperationError::Integrity)?
299 .controllers;
300 controllers.sort();
301 let module = report
302 .module_hash
303 .map(|hash| hash.trim_start_matches("0x").to_ascii_lowercase());
304 if report.id != authority.binding.fleet_subnet_root.to_text()
305 || controllers != authority.root_controllers
306 || module.as_ref() != Some(&authority.root_module_sha256)
307 {
308 return Err(ComponentOperationError::Authority {
309 field: "Root management",
310 });
311 }
312 let RootResponse::Pool(pool) = self.query(
313 plan,
314 &RootRead::Pool(CanisterPoolStatusRequest {
315 start_after: None,
316 limit: 1,
317 }),
318 )?
319 else {
320 return Err(ComponentOperationError::Progress);
321 };
322 Ok(ComponentObservation {
323 authority,
324 ready_assets: pool.ready,
325 })
326 }
327
328 fn progress(
329 &mut self,
330 plan: &ComponentPlanRecord,
331 ) -> Result<ComponentProgressObservation, ComponentOperationError> {
332 let response = self.query(
333 plan,
334 &RootRead::Operation(OperationStatusRequest {
335 operation_id: plan.operation_id,
336 }),
337 );
338 let response = match response {
339 Err(ComponentOperationError::Protocol(error))
340 if error.is_rejected_with(STATE_UNAVAILABLE) =>
341 {
342 return Ok(ComponentProgressObservation { progress: None });
343 }
344 other => other?,
345 };
346 let RootResponse::Operation(status) = response else {
347 return Err(ComponentOperationError::Progress);
348 };
349 let RootOperationStatusResponse::ProvisionComponent(status) = *status else {
350 return Err(ComponentOperationError::Progress);
351 };
352 crate::component_operation::ops::project_progress(plan, status)
353 }
354
355 fn submit(&mut self, plan: &ComponentPlanRecord) -> Result<(), ComponentOperationError> {
356 let principal = plan.authority.binding.fleet_subnet_root;
357 let RootCommandResponse::OperationAccepted(receipt) = call_with_arg(
358 &self.icp,
359 &self.binding(plan, principal)?,
360 principal,
361 protocol::CANIC_ROOT_COMMAND,
362 &RootCommand::ProvisionComponent(RootComponentAllocationRequest {
363 operation_id: plan.operation_id,
364 component_spec: plan.authority.component_spec.clone(),
365 }),
366 )?;
367 if receipt.operation_id != plan.operation_id {
368 return Err(ComponentOperationError::Progress);
369 }
370 Ok(())
371 }
372}