canic-host 0.108.1

Host-side App build, Fleet install, deployment, and release-set library for Canic workspaces
Documentation
//! Module: fleet_install_plan::preflight
//!
//! Responsibility: purely compile canonical fresh-Fleet placement, admission, and funding input.
//! Does not own: input loading, catalog collection, release artifacts, persistence, or effects.
//! Boundary: both plan and install must pass this compiler before any effect boundary.

use super::{initial_placement_policy, model::*};
use crate::component_topology::{PlannedFleetSubnetRootTopologyInput, plan_initial_fleet_topology};

use candid::Principal;

pub const FRESH_FLEET_PREFLIGHT_SCHEMA_VERSION: u16 = 1;

/// Compile one canonical effect-free fresh-Fleet preflight result.
pub fn compile_fresh_fleet_preflight(
    request: FreshFleetPreflightRequest<'_>,
) -> Result<FreshFleetPreflightV1, FreshFleetPreflightError> {
    validate_effect_boundary(request.effects)?;
    if request.config.app_id().as_str() != request.app {
        return Err(FreshFleetPreflightError::AppMismatch {
            configured_app: request.config.app_id().to_string(),
            requested_app: request.app.to_string(),
        });
    }
    validate_coordinator(request.coordinator)?;

    let topology_inputs = request
        .fleet_subnet_roots
        .iter()
        .map(|root| PlannedFleetSubnetRootTopologyInput {
            placement_subnet: root.placement_subnet,
            component_admissions: root.component_admissions.clone(),
            limits: root.limits.clone(),
        })
        .collect();
    let topology_plan = plan_initial_fleet_topology(request.config, topology_inputs)?;
    let mut fleet_subnet_roots = Vec::with_capacity(topology_plan.fleet_subnet_roots.len());

    for topology_root in &topology_plan.fleet_subnet_roots {
        let input = request
            .fleet_subnet_roots
            .iter()
            .find(|input| input.placement_subnet == topology_root.placement_subnet)
            .ok_or(FreshFleetPreflightError::MissingResolvedRoot {
                placement_subnet: topology_root.placement_subnet,
            })?;
        validate_funding(
            &format!("Fleet Subnet Root {}", topology_root.placement_subnet),
            &input.root_creation_funding,
        )?;
        validate_funding(
            &format!(
                "Wasm Store for Fleet Subnet Root {}",
                topology_root.placement_subnet
            ),
            &input.wasm_store_creation_funding,
        )?;
        fleet_subnet_roots.push(FreshFleetSubnetRootPlanV1 {
            placement_subnet: topology_root.placement_subnet,
            placement_cost: input.placement_cost.clone(),
            component_group_placements: input.component_group_placements.clone(),
            component_admissions: topology_root.component_admissions.clone(),
            component_topology_digest: topology_root.component_topology_digest,
            limits: topology_root.limits.clone(),
            funding: input.funding.clone(),
            canister_pool_imports: input.canister_pool_imports.clone(),
            root_creation_funding: input.root_creation_funding.clone(),
            wasm_store_creation_funding: input.wasm_store_creation_funding.clone(),
            initial_component_canisters: 0,
            initial_pool_canisters: 0,
            pool_canister_creations: 0,
            remaining_pool_canisters: 0,
        });
    }

    let component_counts = initial_placement_policy::validate_initial_component_group_assignments(
        request.config,
        &fleet_subnet_roots,
    )
    .map_err(
        |error| FreshFleetPreflightError::InvalidComponentGroupPlacementAssignments {
            reason: error.to_string(),
        },
    )?;
    record_root_counts(&mut fleet_subnet_roots, &component_counts)?;

    Ok(FreshFleetPreflightV1 {
        schema_version: FRESH_FLEET_PREFLIGHT_SCHEMA_VERSION,
        app: request.app.to_string(),
        fleet_name: request.fleet_name.clone(),
        funding_profile: request.coordinator.root_funding.as_ref().map_or(
            canic_core::ids::FleetFundingProfile::SingleSubnet,
            |policy| policy.funding_profile,
        ),
        coordinator: request.coordinator.clone(),
        fleet_subnet_roots,
        build_profile: request.build_profile.target_dir_name().to_string(),
        release_build_id: request.release_build_id,
        effects: request.effects,
        component_topology: topology_plan.component_topology,
    })
}

fn record_root_counts(
    roots: &mut [FreshFleetSubnetRootPlanV1],
    component_counts: &std::collections::BTreeMap<canic_core::ids::SubnetId, u32>,
) -> Result<(), FreshFleetPreflightError> {
    for root in roots {
        let imported = u32::try_from(root.canister_pool_imports.len()).map_err(|_| {
            FreshFleetPreflightError::CountDoesNotFitU32 {
                subject: "root Canister pool import",
            }
        })?;
        let initial_pool = root.limits.canister_pool.minimum_size.max(imported);
        let components = component_counts
            .get(&root.placement_subnet)
            .copied()
            .unwrap_or_default();
        root.initial_component_canisters = components;
        root.initial_pool_canisters = initial_pool;
        root.pool_canister_creations = initial_pool.saturating_sub(imported);
        root.remaining_pool_canisters = initial_pool.saturating_sub(components);
    }
    Ok(())
}

const fn validate_effect_boundary(
    effects: FreshFleetPreflightEffectsV1,
) -> Result<(), FreshFleetPreflightError> {
    if effects.no_effects_started() {
        return Ok(());
    }
    Err(FreshFleetPreflightError::EffectsAlreadyStarted {
        build_started: effects.build_started,
        workspace_mutation_started: effects.workspace_mutation_started,
        ic_mutation_started: effects.ic_mutation_started,
    })
}

pub(super) fn validate_coordinator(
    coordinator: &PlannedFleetCoordinator,
) -> Result<(), FreshFleetPreflightError> {
    if coordinator.coordinator_subnet.as_principal() == &Principal::anonymous() {
        return Err(FreshFleetPreflightError::AnonymousCoordinatorSubnet);
    }
    validate_funding("Fleet Coordinator", &coordinator.creation_funding)
}

pub(super) fn validate_funding(
    owner: &str,
    funding: &PlannedCanisterCreationFunding,
) -> Result<(), FreshFleetPreflightError> {
    let positive = match funding {
        PlannedCanisterCreationFunding::Cycles { cycles } => *cycles > 0,
        PlannedCanisterCreationFunding::Icp { e8s } => *e8s > 0,
    };
    if positive {
        Ok(())
    } else {
        Err(FreshFleetPreflightError::NonPositiveCreationFunding {
            owner: owner.to_string(),
        })
    }
}