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,
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 creation_funding: PlannedCanisterCreationFunding,
66}
67
68///
69/// PlannedFleetSubnetRoot
70///
71/// Canonical root placement, topology, release set, limits, and funding before creation.
72///
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct PlannedFleetSubnetRoot {
77    pub placement_subnet: SubnetId,
78    pub component_admissions: Vec<ComponentSpecAdmission>,
79    pub component_topology_digest: ComponentTopologyDigest,
80    pub initial_release_set: FleetSubnetRootReleaseSet,
81    #[serde(with = "root_limits_document")]
82    pub limits: FleetSubnetRootLimits,
83    pub creation_funding: PlannedCanisterCreationFunding,
84}
85
86///
87/// FleetInstallPlan
88///
89/// Immutable pre-effect authority for one fresh multi-root Fleet installation.
90///
91
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(deny_unknown_fields)]
94pub struct FleetInstallPlan {
95    pub fleet: FleetBinding,
96    pub release_build_id: ReleaseBuildId,
97    pub application_artifact_union_digest: [u8; 32],
98    pub coordinator: PlannedFleetCoordinator,
99    pub fleet_subnet_roots: Vec<PlannedFleetSubnetRoot>,
100}
101
102///
103/// PersistedFleetSubnetRootReleaseSet
104///
105/// Exact durable release-set manifest for one planned root placement.
106///
107
108#[derive(Clone, Debug, Eq, PartialEq)]
109pub struct PersistedFleetSubnetRootReleaseSet {
110    pub placement_subnet: SubnetId,
111    pub manifest: crate::release_set::FleetSubnetRootReleaseSetManifest,
112    pub digest: ReleaseSetDigest,
113    pub path: PathBuf,
114}
115
116///
117/// PersistedFleetInstallPlan
118///
119/// Canonical plan plus every exact immutable root release-set file it admits.
120///
121
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct PersistedFleetInstallPlan {
124    pub plan: FleetInstallPlan,
125    pub digest: [u8; 32],
126    pub path: PathBuf,
127    pub root_release_sets: Vec<PersistedFleetSubnetRootReleaseSet>,
128}
129
130///
131/// FleetInstallPlanRequest
132///
133/// Complete explicit input required to freeze one pre-effect Fleet plan.
134///
135
136pub struct FleetInstallPlanRequest<'a> {
137    pub root: &'a Path,
138    pub config: &'a ConfigModel,
139    pub fleet: FleetBinding,
140    pub release_build_id: ReleaseBuildId,
141    pub coordinator: PlannedFleetCoordinator,
142    pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootInput>,
143}
144
145///
146/// FleetInstallPlanError
147///
148/// Typed rejection while compiling, publishing, or loading Fleet install authority.
149///
150
151#[derive(Debug, ThisError)]
152pub enum FleetInstallPlanError {
153    #[error("Fleet plan App '{fleet_app}' does not match configured App '{configured_app}'")]
154    AppMismatch {
155        configured_app: String,
156        fleet_app: String,
157    },
158
159    #[error("Fleet install plan already exists with different canonical bytes: {path}")]
160    ConflictingPlan { path: PathBuf },
161
162    #[error("root release-set manifest already exists with different canonical bytes: {path}")]
163    ConflictingRootReleaseSet { path: PathBuf },
164
165    #[error("{owner} creation funding amount must be positive")]
166    NonPositiveCreationFunding { owner: String },
167
168    #[error("Coordinator placement Subnet must not be anonymous")]
169    AnonymousCoordinatorSubnet,
170
171    #[error("Fleet Subnet Root placement Subnet must not be anonymous")]
172    AnonymousRootSubnet,
173
174    #[error("Fleet install plan application artifact union digest does not match durable evidence")]
175    ApplicationArtifactUnionDigestMismatch,
176
177    #[error("Fleet Subnet Root plans are not in canonical placement order")]
178    NonCanonicalRootOrder,
179
180    #[error("root {placement_subnet} release build does not match the Fleet install plan")]
181    RootReleaseBuildMismatch { placement_subnet: SubnetId },
182
183    #[error("failed to serialize Fleet install plan: {0}")]
184    PlanSerialization(serde_json::Error),
185
186    #[error("invalid Fleet install plan {path}: {reason}")]
187    InvalidPlanDocument { path: PathBuf, reason: String },
188
189    #[error("invalid root release-set manifest {path}: {reason}")]
190    InvalidRootReleaseSetDocument { path: PathBuf, reason: String },
191
192    #[error("Fleet install plan is missing: {path}")]
193    MissingPlan { path: PathBuf },
194
195    #[error("root release-set manifest is missing: {path}")]
196    MissingRootReleaseSet { path: PathBuf },
197
198    #[error("Fleet install plan exceeds the {maximum_bytes}-byte bound: {actual_bytes}")]
199    PlanTooLarge {
200        maximum_bytes: usize,
201        actual_bytes: usize,
202    },
203
204    #[error("root release-set manifest exceeds the {maximum_bytes}-byte bound: {actual_bytes}")]
205    RootReleaseSetTooLarge {
206        maximum_bytes: usize,
207        actual_bytes: usize,
208    },
209
210    #[error("Fleet install plan is not a regular no-follow file: {path}")]
211    UnsafePlan { path: PathBuf },
212
213    #[error("Fleet install plan lock is not a regular no-follow file: {path}")]
214    UnsafePlanLock { path: PathBuf },
215
216    #[error("root release-set manifest is not a regular no-follow file: {path}")]
217    UnsafeRootReleaseSet { path: PathBuf },
218
219    #[error("failed to access Fleet install authority {path}: {source}")]
220    Io {
221        path: PathBuf,
222        #[source]
223        source: io::Error,
224    },
225
226    #[error(transparent)]
227    ApplicationUnion(#[from] ApplicationArtifactUnionPersistenceError),
228
229    #[error(transparent)]
230    ReleaseBuild(#[from] ReleaseBuildPlanError),
231
232    #[error(transparent)]
233    ReleaseSet(#[from] ApplicationReleaseSetError),
234
235    #[error(transparent)]
236    Topology(#[from] FleetTopologyPlanError),
237
238    #[error(transparent)]
239    ComponentTopology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
240}
241
242#[derive(Deserialize, Serialize)]
243#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
244enum CreationFundingDocument {
245    Cycles { cycles: String },
246    Icp { e8s: u64 },
247}
248
249impl Serialize for PlannedCanisterCreationFunding {
250    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
251    where
252        S: Serializer,
253    {
254        match self {
255            Self::Cycles { cycles } => CreationFundingDocument::Cycles {
256                cycles: cycles.to_string(),
257            },
258            Self::Icp { e8s } => CreationFundingDocument::Icp { e8s: *e8s },
259        }
260        .serialize(serializer)
261    }
262}
263
264impl<'de> Deserialize<'de> for PlannedCanisterCreationFunding {
265    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
266    where
267        D: Deserializer<'de>,
268    {
269        match CreationFundingDocument::deserialize(deserializer)? {
270            CreationFundingDocument::Cycles { cycles } => {
271                let cycles = cycles.parse().map_err(de::Error::custom)?;
272                Ok(Self::Cycles { cycles })
273            }
274            CreationFundingDocument::Icp { e8s } => Ok(Self::Icp { e8s }),
275        }
276    }
277}
278
279mod root_limits_document {
280    use super::*;
281
282    #[derive(Deserialize, Serialize)]
283    #[serde(deny_unknown_fields)]
284    struct CyclesFundingBudgetDocument {
285        window_secs: u64,
286        maximum_cycles: String,
287    }
288
289    #[derive(Deserialize, Serialize)]
290    #[serde(deny_unknown_fields)]
291    struct RootLimitsDocument {
292        maximum_component_instances: u32,
293        maximum_managed_canisters: u32,
294        maximum_registry_bytes: u64,
295        maximum_wasm_store_bytes: u64,
296        cycles_funding: CyclesFundingBudgetDocument,
297    }
298
299    pub(super) fn serialize<S>(
300        limits: &FleetSubnetRootLimits,
301        serializer: S,
302    ) -> Result<S::Ok, S::Error>
303    where
304        S: Serializer,
305    {
306        RootLimitsDocument {
307            maximum_component_instances: limits.maximum_component_instances,
308            maximum_managed_canisters: limits.maximum_managed_canisters,
309            maximum_registry_bytes: limits.maximum_registry_bytes,
310            maximum_wasm_store_bytes: limits.maximum_wasm_store_bytes,
311            cycles_funding: CyclesFundingBudgetDocument {
312                window_secs: limits.cycles_funding.window_secs,
313                maximum_cycles: limits.cycles_funding.maximum_cycles.to_u128().to_string(),
314            },
315        }
316        .serialize(serializer)
317    }
318
319    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<FleetSubnetRootLimits, D::Error>
320    where
321        D: Deserializer<'de>,
322    {
323        let document = RootLimitsDocument::deserialize(deserializer)?;
324        let maximum_cycles = document
325            .cycles_funding
326            .maximum_cycles
327            .parse()
328            .map_err(de::Error::custom)?;
329        Ok(FleetSubnetRootLimits {
330            maximum_component_instances: document.maximum_component_instances,
331            maximum_managed_canisters: document.maximum_managed_canisters,
332            maximum_registry_bytes: document.maximum_registry_bytes,
333            maximum_wasm_store_bytes: document.maximum_wasm_store_bytes,
334            cycles_funding: CyclesFundingBudget {
335                window_secs: document.cycles_funding.window_secs,
336                maximum_cycles: Cycles::new(maximum_cycles),
337            },
338        })
339    }
340}