Skip to main content

canic_host/fleet_install_plan/model/
mod.rs

1//! Module: fleet_install_plan::model
2//!
3//! Responsibility: define passive pre-effect Fleet planning input, authority, and errors.
4//! Does not own: topology compilation, artifact projection, persistence, or external effects.
5//! Boundary: exact cycle amounts use canonical decimal strings only in the durable JSON shape.
6
7use 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///
30/// PlannedCanisterCreationFunding
31///
32/// Exact positive funding method resolved for one host-created initial Canister.
33///
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum PlannedCanisterCreationFunding {
37    Cycles { cycles: u128 },
38    Icp { e8s: u64 },
39}
40
41///
42/// PlannedFleetCoordinator
43///
44/// Exact Coordinator placement and funding resolved before creation.
45///
46
47#[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///
55/// PlannedFleetSubnetRootInput
56///
57/// Explicit host input for one pre-creation Fleet Subnet Root plan.
58///
59
60#[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///
71/// PlannedFleetSubnetRoot
72///
73/// Canonical root placement, topology, release set, limits, and funding before creation.
74///
75
76#[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///
91/// FleetInstallPlan
92///
93/// Immutable pre-effect authority for one fresh multi-root Fleet installation.
94///
95
96#[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///
107/// PersistedFleetSubnetRootReleaseSet
108///
109/// Exact durable release-set manifest for one planned root placement.
110///
111
112#[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///
121/// PersistedFleetInstallPlan
122///
123/// Canonical plan plus every exact immutable root release-set file it admits.
124///
125
126#[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
134///
135/// FleetInstallPlanRequest
136///
137/// Complete explicit input required to freeze one pre-effect Fleet plan.
138///
139
140pub 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///
150/// FleetInstallPlanError
151///
152/// Typed rejection while compiling, publishing, or loading Fleet install authority.
153///
154
155#[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}