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,
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 creation_funding: PlannedCanisterCreationFunding,
66}
67
68#[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#[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#[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#[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
130pub 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#[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}