1use crate::{
8 component_topology::{FleetTopologyPlanError, RootComponentAdmissionInput},
9 release_build::ReleaseBuildPlanError,
10 release_set::{ApplicationArtifactUnionPersistenceError, ApplicationReleaseSetError},
11};
12use std::{
13 io,
14 path::{Path, PathBuf},
15};
16
17use canic_core::{
18 bootstrap::compiled::ConfigModel,
19 cdk::types::{Cycles, Principal},
20 ids::{
21 ComponentSpecAdmission, ComponentTopologyDigest, CyclesFundingBudget, FleetBinding,
22 FleetSubnetRootLimits, FleetSubnetRootReleaseSet, ReleaseBuildId, ReleaseSetDigest,
23 SubnetId,
24 },
25};
26use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
27use thiserror::Error as ThisError;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum PlannedCanisterCreationFunding {
37 Cycles { cycles: u128 },
38 Icp { e8s: u64 },
39}
40
41#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(deny_unknown_fields)]
49pub struct PlannedFleetCoordinator {
50 pub coordinator_subnet: SubnetId,
51 pub creation_funding: PlannedCanisterCreationFunding,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct PlannedFleetSubnetRootInput {
62 pub placement_subnet: SubnetId,
63 pub component_admissions: Vec<RootComponentAdmissionInput>,
64 pub limits: FleetSubnetRootLimits,
65 pub canister_pool_imports: Vec<Principal>,
66 pub root_creation_funding: PlannedCanisterCreationFunding,
67 pub wasm_store_creation_funding: PlannedCanisterCreationFunding,
68}
69
70#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct PlannedFleetSubnetRoot {
79 pub placement_subnet: SubnetId,
80 pub component_admissions: Vec<ComponentSpecAdmission>,
81 pub component_topology_digest: ComponentTopologyDigest,
82 pub initial_release_set: FleetSubnetRootReleaseSet,
83 #[serde(with = "root_limits_document")]
84 pub limits: FleetSubnetRootLimits,
85 pub canister_pool_imports: Vec<Principal>,
86 pub root_creation_funding: PlannedCanisterCreationFunding,
87 pub wasm_store_creation_funding: PlannedCanisterCreationFunding,
88}
89
90#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct FleetInstallPlan {
99 pub fleet: FleetBinding,
100 pub release_build_id: ReleaseBuildId,
101 pub application_artifact_union_digest: [u8; 32],
102 pub coordinator: PlannedFleetCoordinator,
103 pub fleet_subnet_roots: Vec<PlannedFleetSubnetRoot>,
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct PersistedFleetSubnetRootReleaseSet {
114 pub placement_subnet: SubnetId,
115 pub manifest: crate::release_set::FleetSubnetRootReleaseSetManifest,
116 pub digest: ReleaseSetDigest,
117 pub path: PathBuf,
118}
119
120#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct PersistedFleetInstallPlan {
128 pub plan: FleetInstallPlan,
129 pub digest: [u8; 32],
130 pub path: PathBuf,
131 pub root_release_sets: Vec<PersistedFleetSubnetRootReleaseSet>,
132}
133
134pub struct FleetInstallPlanRequest<'a> {
141 pub root: &'a Path,
142 pub config: &'a ConfigModel,
143 pub fleet: FleetBinding,
144 pub release_build_id: ReleaseBuildId,
145 pub coordinator: PlannedFleetCoordinator,
146 pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootInput>,
147}
148
149#[derive(Debug, ThisError)]
156pub enum FleetInstallPlanError {
157 #[error("Fleet plan App '{fleet_app}' does not match configured App '{configured_app}'")]
158 AppMismatch {
159 configured_app: String,
160 fleet_app: String,
161 },
162
163 #[error("Fleet install plan already exists with different canonical bytes: {path}")]
164 ConflictingPlan { path: PathBuf },
165
166 #[error("root release-set manifest already exists with different canonical bytes: {path}")]
167 ConflictingRootReleaseSet { path: PathBuf },
168
169 #[error("{owner} creation funding amount must be positive")]
170 NonPositiveCreationFunding { owner: String },
171
172 #[error("Coordinator placement Subnet must not be anonymous")]
173 AnonymousCoordinatorSubnet,
174
175 #[error("Fleet Subnet Root placement Subnet must not be anonymous")]
176 AnonymousRootSubnet,
177
178 #[error("Fleet install plan application artifact union digest does not match durable evidence")]
179 ApplicationArtifactUnionDigestMismatch,
180
181 #[error("Fleet Subnet Root plans are not in canonical placement order")]
182 NonCanonicalRootOrder,
183
184 #[error("root {placement_subnet} release build does not match the Fleet install plan")]
185 RootReleaseBuildMismatch { placement_subnet: SubnetId },
186
187 #[error("failed to serialize Fleet install plan: {0}")]
188 PlanSerialization(serde_json::Error),
189
190 #[error("invalid Fleet install plan {path}: {reason}")]
191 InvalidPlanDocument { path: PathBuf, reason: String },
192
193 #[error("invalid root release-set manifest {path}: {reason}")]
194 InvalidRootReleaseSetDocument { path: PathBuf, reason: String },
195
196 #[error("Fleet install plan is missing: {path}")]
197 MissingPlan { path: PathBuf },
198
199 #[error("root release-set manifest is missing: {path}")]
200 MissingRootReleaseSet { path: PathBuf },
201
202 #[error("Fleet install plan exceeds the {maximum_bytes}-byte bound: {actual_bytes}")]
203 PlanTooLarge {
204 maximum_bytes: usize,
205 actual_bytes: usize,
206 },
207
208 #[error("root release-set manifest exceeds the {maximum_bytes}-byte bound: {actual_bytes}")]
209 RootReleaseSetTooLarge {
210 maximum_bytes: usize,
211 actual_bytes: usize,
212 },
213
214 #[error("Fleet install plan is not a regular no-follow file: {path}")]
215 UnsafePlan { path: PathBuf },
216
217 #[error("Fleet install plan lock is not a regular no-follow file: {path}")]
218 UnsafePlanLock { path: PathBuf },
219
220 #[error("root release-set manifest is not a regular no-follow file: {path}")]
221 UnsafeRootReleaseSet { path: PathBuf },
222
223 #[error("failed to access Fleet install authority {path}: {source}")]
224 Io {
225 path: PathBuf,
226 #[source]
227 source: io::Error,
228 },
229
230 #[error(transparent)]
231 ApplicationUnion(#[from] ApplicationArtifactUnionPersistenceError),
232
233 #[error(transparent)]
234 ReleaseBuild(#[from] ReleaseBuildPlanError),
235
236 #[error(transparent)]
237 ReleaseSet(#[from] ApplicationReleaseSetError),
238
239 #[error(transparent)]
240 Topology(#[from] FleetTopologyPlanError),
241
242 #[error(transparent)]
243 ComponentTopology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
244}
245
246#[derive(Deserialize, Serialize)]
247#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
248enum CreationFundingDocument {
249 Cycles { cycles: String },
250 Icp { e8s: u64 },
251}
252
253impl Serialize for PlannedCanisterCreationFunding {
254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255 where
256 S: Serializer,
257 {
258 match self {
259 Self::Cycles { cycles } => CreationFundingDocument::Cycles {
260 cycles: cycles.to_string(),
261 },
262 Self::Icp { e8s } => CreationFundingDocument::Icp { e8s: *e8s },
263 }
264 .serialize(serializer)
265 }
266}
267
268impl<'de> Deserialize<'de> for PlannedCanisterCreationFunding {
269 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
270 where
271 D: Deserializer<'de>,
272 {
273 match CreationFundingDocument::deserialize(deserializer)? {
274 CreationFundingDocument::Cycles { cycles } => {
275 let cycles = cycles.parse().map_err(de::Error::custom)?;
276 Ok(Self::Cycles { cycles })
277 }
278 CreationFundingDocument::Icp { e8s } => Ok(Self::Icp { e8s }),
279 }
280 }
281}
282
283mod root_limits_document {
284 use super::*;
285
286 #[derive(Deserialize, Serialize)]
287 #[serde(deny_unknown_fields)]
288 struct CyclesFundingBudgetDocument {
289 window_secs: u64,
290 maximum_cycles: String,
291 }
292
293 #[derive(Deserialize, Serialize)]
294 #[serde(deny_unknown_fields)]
295 struct CanisterPoolDocument {
296 minimum_size: u32,
297 maximum_size: u32,
298 canister_cycles: String,
299 }
300
301 #[derive(Deserialize, Serialize)]
302 #[serde(deny_unknown_fields)]
303 struct RootLimitsDocument {
304 maximum_component_instances: u32,
305 maximum_managed_canisters: u32,
306 maximum_registry_bytes: u64,
307 maximum_wasm_store_bytes: u64,
308 canister_pool: CanisterPoolDocument,
309 cycles_funding: CyclesFundingBudgetDocument,
310 }
311
312 pub(super) fn serialize<S>(
313 limits: &FleetSubnetRootLimits,
314 serializer: S,
315 ) -> Result<S::Ok, S::Error>
316 where
317 S: Serializer,
318 {
319 RootLimitsDocument {
320 maximum_component_instances: limits.maximum_component_instances,
321 maximum_managed_canisters: limits.maximum_managed_canisters,
322 maximum_registry_bytes: limits.maximum_registry_bytes,
323 maximum_wasm_store_bytes: limits.maximum_wasm_store_bytes,
324 canister_pool: CanisterPoolDocument {
325 minimum_size: limits.canister_pool.minimum_size,
326 maximum_size: limits.canister_pool.maximum_size,
327 canister_cycles: limits.canister_pool.canister_cycles.to_u128().to_string(),
328 },
329 cycles_funding: CyclesFundingBudgetDocument {
330 window_secs: limits.cycles_funding.window_secs,
331 maximum_cycles: limits.cycles_funding.maximum_cycles.to_u128().to_string(),
332 },
333 }
334 .serialize(serializer)
335 }
336
337 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<FleetSubnetRootLimits, D::Error>
338 where
339 D: Deserializer<'de>,
340 {
341 let document = RootLimitsDocument::deserialize(deserializer)?;
342 let maximum_cycles = document
343 .cycles_funding
344 .maximum_cycles
345 .parse()
346 .map_err(de::Error::custom)?;
347 let canister_cycles = document
348 .canister_pool
349 .canister_cycles
350 .parse()
351 .map_err(de::Error::custom)?;
352 Ok(FleetSubnetRootLimits {
353 maximum_component_instances: document.maximum_component_instances,
354 maximum_managed_canisters: document.maximum_managed_canisters,
355 maximum_registry_bytes: document.maximum_registry_bytes,
356 maximum_wasm_store_bytes: document.maximum_wasm_store_bytes,
357 canister_pool: canic_core::ids::FleetSubnetCanisterPoolConfig {
358 minimum_size: document.canister_pool.minimum_size,
359 maximum_size: document.canister_pool.maximum_size,
360 canister_cycles: Cycles::new(canister_cycles),
361 },
362 cycles_funding: CyclesFundingBudget {
363 window_secs: document.cycles_funding.window_secs,
364 maximum_cycles: Cycles::new(maximum_cycles),
365 },
366 })
367 }
368}