canic_host/fleet_ensure/generate/capacity_import/
mod.rs1#[cfg(test)]
7pub(in crate::fleet_ensure) mod tests;
8
9use crate::fleet_ensure::{
10 generate::{
11 EstateSeed, FleetGenerateError, FleetSource, MAX_GENERATOR_INPUT_BYTES,
12 canonical_recovery_controllers, validate_identity_seed,
13 },
14 model::capacity_import::{CapacityImportPlanRecord, operation::CapacityImportPublicationKind},
15 ops::capacity_import::{CapacityImportReviewError, verify_review},
16 view::capacity_import::{CapacityImportDocumentView, CapacityImportInventoryView},
17};
18use candid::Principal;
19use sha2_host::{Digest, Sha256};
20use thiserror::Error;
21
22#[derive(Debug, Error)]
24pub enum CapacityImportInventoryError {
25 #[error("capacity import generator input exceeds its byte bound")]
26 TooLarge,
27 #[error("capacity import generator input is not UTF-8")]
28 Utf8(#[from] std::str::Utf8Error),
29 #[error("capacity import generator input has an invalid current schema")]
30 Decode(#[from] toml::de::Error),
31 #[error("capacity import generator replacement cannot be encoded")]
32 Encode(#[from] toml::ser::Error),
33 #[error("capacity import generator inputs differ from the reviewed retained estate")]
34 AuthorityMismatch,
35 #[error("capacity import candidate already occurs in the destination seed")]
36 AlreadyPresent,
37 #[error(transparent)]
38 Review(#[from] CapacityImportReviewError),
39 #[error(transparent)]
40 Generation(Box<FleetGenerateError>),
41}
42
43impl From<FleetGenerateError> for CapacityImportInventoryError {
44 fn from(error: FleetGenerateError) -> Self {
45 Self::Generation(Box::new(error))
46 }
47}
48
49pub fn prepare_capacity_import_inventory(
54 plan: &CapacityImportPlanRecord,
55 policy_bytes: &[u8],
56 seed_bytes: &[u8],
57) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
58 prepare_inventory(
59 plan,
60 policy_bytes,
61 seed_bytes,
62 CapacityImportPublicationKind::ExtendEstate,
63 )
64}
65
66pub fn prepare_initial_import_inventory(
69 plan: &CapacityImportPlanRecord,
70 policy_bytes: &[u8],
71 seed_bytes: &[u8],
72) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
73 prepare_inventory(
74 plan,
75 policy_bytes,
76 seed_bytes,
77 CapacityImportPublicationKind::InitializeEstate,
78 )
79}
80
81fn prepare_inventory(
82 plan: &CapacityImportPlanRecord,
83 policy_bytes: &[u8],
84 seed_bytes: &[u8],
85 kind: CapacityImportPublicationKind,
86) -> Result<CapacityImportInventoryView, CapacityImportInventoryError> {
87 verify_review(plan, plan.plan_sha256)?;
88 let policy_text = input_text(policy_bytes)?;
89 let seed_text = input_text(seed_bytes)?;
90 let mut source: FleetSource = toml::from_str(policy_text)?;
91 let mut seed: EstateSeed = toml::from_str(seed_text)?;
92 let mut policy_document: toml::Value = toml::from_str(policy_text)?;
93 let mut seed_document: toml::Value = toml::from_str(seed_text)?;
94 let authority = &plan.authority;
95 let mut recovery = authority
96 .recovery_controllers
97 .iter()
98 .map(Principal::to_text)
99 .collect::<Vec<_>>();
100 recovery.sort();
101 let configured_recovery =
102 canonical_recovery_controllers(&source.recovery_controllers, authority.operator)?;
103 let estate_matches = seed.schema_version == 1
104 && !seed.fresh_estate
105 && seed.fleet_id == authority.fleet.fleet.fleet_id
106 && seed.coordinator == authority.coordinator.to_text();
107 let policy_matches = source.schema_version == 1
108 && source.operator == authority.operator.to_text()
109 && configured_recovery == recovery;
110 if !estate_matches || !policy_matches {
111 return Err(CapacityImportInventoryError::AuthorityMismatch);
112 }
113 validate_identity_seed(&source, &seed)?;
114 let subnet = authority.subnet.into_principal().to_text();
115 let seed_index = seed
116 .roots
117 .iter()
118 .position(|root| root.root == authority.root.to_text() && root.placement_subnet == subnet)
119 .ok_or(CapacityImportInventoryError::AuthorityMismatch)?;
120 let source_index = source
121 .fleet_subnet_roots
122 .iter()
123 .position(|root| root.placement_subnet == subnet)
124 .ok_or(CapacityImportInventoryError::AuthorityMismatch)?;
125 let imports = &mut seed.roots[seed_index].pool_imports;
126 match kind {
127 CapacityImportPublicationKind::ExtendEstate => {
128 for candidate in &plan.sources {
129 let id = candidate.binding.canister_id.to_text();
130 if imports.contains(&id) {
131 return Err(CapacityImportInventoryError::AlreadyPresent);
132 }
133 imports.push(id);
134 }
135 }
136 CapacityImportPublicationKind::InitializeEstate => {
137 let expected = plan
138 .sources
139 .iter()
140 .map(|source| source.binding.canister_id.to_text())
141 .collect::<std::collections::BTreeSet<_>>();
142 let supplied = imports
143 .iter()
144 .cloned()
145 .collect::<std::collections::BTreeSet<_>>();
146 if imports.len() != supplied.len() || supplied != expected {
147 return Err(CapacityImportInventoryError::AuthorityMismatch);
148 }
149 }
150 }
151 source.fleet_subnet_roots[source_index]
152 .canister_pool
153 .imports
154 .clone_from(imports);
155 let replacement_imports =
156 toml::Value::Array(imports.iter().cloned().map(toml::Value::String).collect());
157 validate_identity_seed(&source, &seed)?;
160 let source_root = array_entry(&mut policy_document, "fleet_subnet_roots", source_index)?;
161 source_root
162 .get_mut("canister_pool")
163 .and_then(toml::Value::as_table_mut)
164 .ok_or(CapacityImportInventoryError::AuthorityMismatch)?
165 .insert("imports".to_string(), replacement_imports.clone());
166 array_entry(&mut seed_document, "roots", seed_index)?
167 .as_table_mut()
168 .ok_or(CapacityImportInventoryError::AuthorityMismatch)?
169 .insert("pool_imports".to_string(), replacement_imports);
170 Ok(CapacityImportInventoryView {
171 plan_sha256: plan.plan_sha256,
172 policy: replacement(policy_bytes, &policy_document)?,
173 seed: replacement(seed_bytes, &seed_document)?,
174 })
175}
176
177fn input_text(bytes: &[u8]) -> Result<&str, CapacityImportInventoryError> {
178 if bytes.len() > MAX_GENERATOR_INPUT_BYTES {
179 return Err(CapacityImportInventoryError::TooLarge);
180 }
181 Ok(std::str::from_utf8(bytes)?)
182}
183
184fn array_entry<'a>(
185 document: &'a mut toml::Value,
186 name: &str,
187 index: usize,
188) -> Result<&'a mut toml::Value, CapacityImportInventoryError> {
189 document
190 .get_mut(name)
191 .and_then(toml::Value::as_array_mut)
192 .and_then(|entries| entries.get_mut(index))
193 .ok_or(CapacityImportInventoryError::AuthorityMismatch)
194}
195
196fn replacement(
197 original: &[u8],
198 document: &toml::Value,
199) -> Result<CapacityImportDocumentView, CapacityImportInventoryError> {
200 let replacement = if document == &toml::from_str::<toml::Value>(input_text(original)?)? {
201 original.to_vec()
202 } else {
203 toml::to_string_pretty(document)?.into_bytes()
204 };
205 if replacement.len() > MAX_GENERATOR_INPUT_BYTES {
206 return Err(CapacityImportInventoryError::TooLarge);
207 }
208 Ok(CapacityImportDocumentView {
209 before_sha256: Sha256::digest(original).into(),
210 after_sha256: Sha256::digest(&replacement).into(),
211 replacement,
212 })
213}