use crate::{
deployment_truth::{DeploymentReceiptV1, FreshFleetInstallDecisionReceiptV1},
fleet_install_input::{
ResolvedFleetInstallInput, load_and_resolve_fleet_install_input,
load_and_resolve_fleet_install_input_for_preflight,
},
fleet_install_plan::{
FleetInstallPlanRequest, FreshFleetDecisionAuthorityRequest,
FreshFleetDeploymentPlanRequest, FreshFleetDeploymentPlanV1, FreshFleetPreflightEffectsV1,
FreshFleetPreflightRequest, FreshFleetPreflightV1, PersistedFleetInstallPlan,
PlannedCanisterCreationFunding, compile_and_persist_fleet_install_plan,
compile_fresh_fleet_deployment_plan, compile_fresh_fleet_preflight,
fresh_fleet_maximum_operator_debit, load_fresh_fleet_decision_authority,
observe_fresh_fleet_operator_funding,
},
network::resolve_canonical_network_id_from_root,
release_set::{AppConfigSnapshot, icp_root, workspace_root},
};
use config_selection::resolve_install_config_path;
use sha2::{Digest, Sha256};
use std::{
fmt,
path::{Path, PathBuf},
time::{Duration, Instant},
};
use thiserror::Error as ThisError;
mod build_network;
mod build_snapshot;
mod build_targets;
mod capabilities;
mod clock;
mod commands;
mod config_selection;
mod coordinator_install;
mod coordinator_install_journal;
mod current_execution;
mod deployment_truth_gate;
mod execution_preflight;
mod fleet_catalog_closeout;
mod fleet_catalog_publication;
mod fleet_component_provisioning_install;
mod fleet_component_provisioning_journal;
mod fleet_component_provisioning_plan;
mod fleet_install_session;
mod fleet_registry_activation;
mod fleet_registry_activation_journal;
mod fleet_subnet_root_component_registry_preparation;
mod fleet_subnet_root_install;
mod fleet_subnet_root_install_journal;
mod fleet_subnet_root_registry_join;
mod fleet_subnet_root_registry_mirror_activation;
mod fleet_subnet_root_registry_sync;
mod fleet_subnet_root_store_bootstrap;
mod icp_context;
mod identity;
mod operations;
mod options;
mod output;
mod phase_receipts;
mod plan_artifacts;
mod preparation;
mod receipt_io;
mod reused_build;
mod timing;
mod truth_check;
use crate::release_build::{
PlannedReleaseBuild, ReleaseBuildPlanError, load_finalized_release_build,
plan_release_build_for_profile,
};
use build_network::resolve_install_build_context;
use build_snapshot::resolve_install_snapshot;
pub use config_selection::{
ConfigDiscoveryError, current_canic_workspace_root, discover_canic_config_choices,
discover_canic_workspace_root_from, discover_workspace_canic_config_choices,
select_discovered_app_config_path, workspace_app_roots,
};
use coordinator_install::install_and_verify_fleet_coordinator;
use fleet_component_provisioning_install::{
InstallFleetComponentsRequest, install_fleet_components_and_publish_catalog,
};
use fleet_registry_activation::{ActivateFleetRegistryRequest, activate_and_verify_fleet_registry};
use fleet_registry_activation_journal::load_verified_installed_registry;
use fleet_subnet_root_component_registry_preparation::{
PrepareFleetSubnetRootComponentRegistriesRequest,
prepare_and_verify_fleet_subnet_root_component_registries,
};
use fleet_subnet_root_install::install_and_verify_fleet_subnet_roots;
use fleet_subnet_root_registry_join::register_and_verify_fleet_subnet_roots_joining;
use fleet_subnet_root_registry_mirror_activation::{
ActivateFleetSubnetRootRegistryMirrorsRequest,
activate_and_verify_fleet_subnet_root_registry_mirrors,
};
use fleet_subnet_root_registry_sync::{
SynchronizeFleetSubnetRootsRequest, synchronize_and_verify_fleet_subnet_roots,
};
use fleet_subnet_root_store_bootstrap::bootstrap_and_verify_fleet_subnet_root_stores;
use icp_context::InstallIcpContext;
use identity::resolve_install_identity;
pub use options::InstallRootOptions;
use output::{TerminalStyle, print_install_timing_summary};
use phase_receipts::{
CompletedInstallPhase, InstallReceiptScope, write_completed_install_phase_receipt,
};
use plan_artifacts::emit_manifest_with_phase;
use preparation::prepare_install_deployment_truth;
pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
pub(crate) fn load_verified_installed_fleet_registry(
fleet_install_plan: &PersistedFleetInstallPlan,
) -> Result<canic_core::dto::fleet_registry::FleetRegistry, String> {
load_verified_installed_registry(fleet_install_plan).map_err(|error| error.to_string())
}
pub(super) fn root_component_provisioning_operation_id(install_operation_id: [u8; 32]) -> [u8; 32] {
root_install_phase_operation_id(install_operation_id, b"component-provisioning")
}
pub(super) fn root_registry_synchronization_operation_id(
install_operation_id: [u8; 32],
) -> [u8; 32] {
root_install_phase_operation_id(install_operation_id, b"registry-synchronization")
}
pub(super) fn root_store_adoption_operation_id(install_operation_id: [u8; 32]) -> [u8; 32] {
root_install_phase_operation_id(install_operation_id, b"store-adoption")
}
pub(super) fn root_store_bootstrap_operation_id(install_operation_id: [u8; 32]) -> [u8; 32] {
root_install_phase_operation_id(install_operation_id, b"store-bootstrap")
}
fn root_install_phase_operation_id(install_operation_id: [u8; 32], phase: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"canic.fleet-install.root-operation.v1\0");
hasher.update(phase);
hasher.update([0]);
hasher.update(install_operation_id);
let mut operation_id: [u8; 32] = hasher.finalize().into();
if operation_id == [0; 32] {
operation_id[31] = 1;
}
operation_id
}
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstallRootBlockKind {
DeploymentExecutionPreflight,
DeploymentTruth,
}
#[derive(Debug, ThisError)]
#[error("{message}")]
pub struct InstallRootBlockedError {
kind: InstallRootBlockKind,
message: String,
}
impl InstallRootBlockedError {
pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
Self { kind, message }
}
#[must_use]
pub const fn kind(&self) -> InstallRootBlockKind {
self.kind
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstallRootPhase {
WorkspaceDiscovery,
IcpProjectDiscovery,
Configuration,
BuildInputs,
Identity,
Preparation,
Manifest,
Planning,
Activation,
}
impl fmt::Display for InstallRootPhase {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::WorkspaceDiscovery => "workspace discovery",
Self::IcpProjectDiscovery => "ICP project discovery",
Self::Configuration => "configuration selection",
Self::BuildInputs => "build input validation",
Self::Identity => "deployment identity resolution",
Self::Preparation => "deployment preparation",
Self::Manifest => "manifest emission",
Self::Planning => "Fleet installation planning",
Self::Activation => "root activation",
})
}
}
#[derive(Debug, ThisError)]
#[error("root install failed during {phase}: {source}")]
pub struct InstallRootError {
phase: InstallRootPhase,
#[source]
source: Box<dyn std::error::Error>,
}
impl InstallRootError {
pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
where
E: std::error::Error + 'static,
{
Self {
phase,
source: Box::new(source),
}
}
fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
move |source| Self { phase, source }
}
#[must_use]
pub const fn phase(&self) -> InstallRootPhase {
self.phase
}
}
#[derive(Debug, ThisError)]
#[error("fresh Fleet installation requires --fleet-input <PATH>")]
struct MissingFleetInstallInputError;
#[derive(Debug, ThisError)]
#[error("preflight App {preflight} differs from build snapshot App {build_snapshot}")]
struct InstallAppSnapshotChangedError {
preflight: String,
build_snapshot: String,
}
#[derive(Debug, ThisError)]
#[error("fresh-Fleet plan digest differs: expected {expected}, observed {observed}")]
struct FreshFleetPlanDigestMismatchError {
expected: String,
observed: String,
}
#[derive(Debug)]
struct PreparedFreshFleetDecision {
app_id: String,
fleet_name: String,
input: ResolvedFleetInstallInput,
plan: FreshFleetDeploymentPlanV1,
}
#[derive(Clone, Copy)]
enum FleetCatalogAcquisition {
CacheOnly,
RefreshMissingOrInvalid,
}
pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
let workspace_root = current_canic_workspace_root()?;
let choices = config_selection::discover_workspace_canic_config_choices(&workspace_root)?;
if !choices.is_empty() {
return Ok(choices);
}
let icp_root = icp_root()?;
if icp_root != workspace_root {
return config_selection::discover_workspace_canic_config_choices(&icp_root);
}
Ok(choices)
}
pub fn install_root(mut options: InstallRootOptions) -> Result<(), InstallRootError> {
let (workspace_root, icp_root) = resolve_current_install_roots(&options)?;
let config_path = current_install_config_path(&icp_root, &options)?;
let (fresh_fleet, icp_context) =
prepare_and_admit_current_fresh_fleet(&workspace_root, &icp_root, &config_path, &options)?;
let PreparedFreshFleetDecision {
app_id,
fleet_name,
input: resolved_fleet_install_input,
plan: fresh_fleet_plan,
} = fresh_fleet;
options.admitted_fresh_fleet_plan_digest = Some(fresh_fleet_plan.plan_digest.clone());
let (build_context, install_snapshot) = current_install_build_inputs(
&workspace_root,
&icp_root,
&config_path,
&icp_context,
&options,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
options.build_profile = Some(build_context.profile);
require_install_app_unchanged(&app_id, &install_snapshot)?;
let total_started_at = Instant::now();
let environment = options.environment.as_str();
let artifact_root = if options.deployment_plan_override.is_some() {
crate::release_set::artifact_root_path(&icp_root, options.artifact_environment())
} else {
build_context.artifact_root()
};
let execution_context = current_execution::current_install_execution_context_at_root(
&workspace_root,
&icp_root,
&artifact_root,
);
print_install_identity(&app_id, &fleet_name);
let prepared = prepare_install_deployment_truth(
&options,
&icp_context,
&config_path,
&fleet_name,
&execution_context,
&build_context,
&install_snapshot,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
let mut timings = prepared.timings;
let emitted_manifest = emit_manifest_with_phase(
&icp_root,
&install_snapshot,
&prepared.build_outputs,
&prepared.infrastructure_build_outputs,
prepared.plan_artifacts.as_ref(),
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
timings.emit_manifest = emitted_manifest.duration;
let finalized_release_build =
require_finalized_release_build(emitted_manifest.finalized_release_build)?;
recheck_fresh_fleet_operator_funding(&icp_context, &fresh_fleet_plan)?;
let planned_install = plan_current_fleet_install(CurrentFleetInstallPlanRequest {
icp_root: &icp_root,
environment,
fleet_name: &fleet_name,
app_id: &app_id,
config_path: &config_path,
finalized_release_build: &finalized_release_build,
input: resolved_fleet_install_input,
fresh_fleet_plan: &fresh_fleet_plan,
})?;
let fresh_fleet_receipt_decision = FreshFleetInstallDecisionReceiptV1 {
plan_digest: fresh_fleet_plan.plan_digest.clone(),
catalog: fresh_fleet_plan.authority.catalog,
};
let receipt_scope = InstallReceiptScope {
icp_root: &icp_root,
fleet: planned_install.fleet(),
check: &prepared.deployment_truth_check,
execution_context: Some(&execution_context),
fresh_fleet_decision: Some(&fresh_fleet_receipt_decision),
};
persist_current_pre_root_receipts(
receipt_scope,
&prepared.pre_activation_receipts,
prepared.build_phase,
emitted_manifest.phase,
)?;
let icp_context = icp_context.with_local_replica(build_context.local_replica);
print_paid_effect_placement_warnings(&planned_install.plan.plan);
let activation_started_at = Instant::now();
install_current_fleet_infrastructure(&icp_context, &config_path, &planned_install)?;
timings.activate_fleet = activation_started_at.elapsed();
print_install_timing_summary(&timings, total_started_at.elapsed());
Ok(())
}
fn prepare_and_admit_current_fresh_fleet(
workspace_root: &Path,
icp_root: &Path,
config_path: &Path,
options: &InstallRootOptions,
) -> Result<(PreparedFreshFleetDecision, InstallIcpContext), InstallRootError> {
let icp_context =
InstallIcpContext::new(&options.icp_executable, icp_root, &options.environment);
let announced_fresh_fleet = prepare_current_fresh_fleet_preflight(
workspace_root,
icp_root,
config_path,
options,
&icp_context,
FleetCatalogAcquisition::RefreshMissingOrInvalid,
)?;
print_fresh_fleet_decision(&announced_fresh_fleet.plan);
let fresh_fleet = prepare_current_fresh_fleet_preflight(
workspace_root,
icp_root,
config_path,
options,
&icp_context,
FleetCatalogAcquisition::CacheOnly,
)?;
require_recompiled_fresh_fleet_plan(&announced_fresh_fleet.plan, &fresh_fleet.plan)?;
Ok((fresh_fleet, icp_context))
}
fn prepare_current_fresh_fleet_preflight(
workspace_root: &Path,
icp_root: &Path,
config_path: &Path,
options: &InstallRootOptions,
icp_context: &InstallIcpContext,
catalog_acquisition: FleetCatalogAcquisition,
) -> Result<PreparedFreshFleetDecision, InstallRootError> {
let config = AppConfigSnapshot::load(config_path)
.map_err(|source| InstallRootError::new(InstallRootPhase::Configuration, source))?;
let (app_id, fleet_name) = resolve_install_identity(options, config_path, config.app_id())
.map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
let canonical_network_id =
resolve_canonical_network_id_from_root(icp_root, &options.environment)
.map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
let input = resolve_current_fleet_install_input(
icp_root,
&options.environment,
options,
catalog_acquisition,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
let (build_profile, release_build_id, recovered_plan_digest) =
current_install_preflight_release_source(
icp_root,
canonical_network_id,
&fleet_name,
&app_id,
options,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
let preflight = compile_current_fresh_fleet_preflight(
&config,
&app_id,
&fleet_name,
&input,
build_profile,
release_build_id,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
let maximum_operator_debit = fresh_fleet_maximum_operator_debit(&preflight)
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
let operator = observe_fresh_fleet_operator_funding(
icp_context.cli(),
&input.operator_principal,
&maximum_operator_debit,
)
.map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
let authority = load_fresh_fleet_decision_authority(FreshFleetDecisionAuthorityRequest {
workspace_root,
icp_root,
config: &config,
requested_environment: &options.environment,
canonical_network_id,
release_build_id,
fleet_input: &input,
operator: &operator,
})
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
let plan = compile_fresh_fleet_deployment_plan(FreshFleetDeploymentPlanRequest {
preflight,
authority,
})
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
require_fresh_fleet_plan_digest(
options.expected_fresh_fleet_plan_digest.as_deref(),
&plan.plan_digest,
)
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
require_fresh_fleet_plan_digest(recovered_plan_digest.as_deref(), &plan.plan_digest)
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
Ok(PreparedFreshFleetDecision {
app_id,
fleet_name,
input,
plan,
})
}
fn recheck_fresh_fleet_operator_funding(
icp_context: &InstallIcpContext,
plan: &FreshFleetDeploymentPlanV1,
) -> Result<(), InstallRootError> {
let operator = observe_fresh_fleet_operator_funding(
icp_context.cli(),
&plan.authority.operator.principal,
&plan.maximum_operator_debit,
)
.map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
let mut authority = plan.authority.clone();
authority.operator = operator;
let rechecked = compile_fresh_fleet_deployment_plan(FreshFleetDeploymentPlanRequest {
preflight: plan.preflight.clone(),
authority,
})
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))?;
require_recompiled_fresh_fleet_plan(plan, &rechecked)
}
fn install_current_fleet_infrastructure(
icp_context: &InstallIcpContext,
config_path: &Path,
planned: &PlannedCurrentFleetInstall,
) -> Result<(), InstallRootError> {
let (coordinator, _) =
install_current_fleet_coordinator(icp_context, config_path, &planned.plan)?;
let _ = install_current_fleet_subnet_roots(
icp_context,
config_path,
planned,
coordinator.coordinator,
)?;
bootstrap_and_verify_fleet_subnet_root_stores(
icp_context,
config_path,
&planned.plan,
coordinator.coordinator,
planned.session.operation_id,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
let joining_version = register_and_verify_fleet_subnet_roots_joining(
icp_context,
config_path,
&planned.plan,
coordinator.coordinator,
planned.session.operation_id,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
synchronize_and_verify_fleet_subnet_roots(SynchronizeFleetSubnetRootsRequest {
icp: icp_context,
config_path,
fleet_install_plan: &planned.plan,
coordinator: coordinator.coordinator,
install_operation_id: planned.session.operation_id,
joining_version: joining_version.clone(),
})
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
let active = activate_and_verify_fleet_registry(ActivateFleetRegistryRequest {
icp: icp_context,
config_path,
fleet_install_plan: &planned.plan,
coordinator: coordinator.coordinator,
install_operation_id: planned.session.operation_id,
joining_version: joining_version.clone(),
})
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
activate_and_verify_fleet_subnet_root_registry_mirrors(
ActivateFleetSubnetRootRegistryMirrorsRequest {
icp: icp_context,
config_path,
fleet_install_plan: &planned.plan,
coordinator: coordinator.coordinator,
install_operation_id: planned.session.operation_id,
joining_version,
active_registry: &active.registry,
active_version: active.version.clone(),
},
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
prepare_current_fleet_subnet_root_component_registries(
icp_context,
config_path,
planned,
coordinator.coordinator,
)?;
install_fleet_components_and_publish_catalog(InstallFleetComponentsRequest {
icp: icp_context,
config_path,
fleet_name: planned.session.fleet_name.clone(),
fleet_install_plan: &planned.plan,
coordinator: coordinator.coordinator,
install_operation_id: planned.session.operation_id,
initial_active_registry: &active.registry,
})
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
}
fn prepare_current_fleet_subnet_root_component_registries(
icp_context: &InstallIcpContext,
config_path: &Path,
planned: &PlannedCurrentFleetInstall,
coordinator: canic_core::cdk::types::Principal,
) -> Result<(), InstallRootError> {
prepare_and_verify_fleet_subnet_root_component_registries(
PrepareFleetSubnetRootComponentRegistriesRequest {
icp: icp_context,
config_path,
fleet_install_plan: &planned.plan,
coordinator,
install_operation_id: planned.session.operation_id,
},
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
}
fn resolve_current_install_roots(
options: &InstallRootOptions,
) -> Result<(PathBuf, PathBuf), InstallRootError> {
let workspace_root = workspace_root()
.map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
let icp_root = match &options.icp_root {
Some(path) => path.canonicalize().map_err(|source| {
InstallRootError::new(InstallRootPhase::IcpProjectDiscovery, source)
})?,
None => icp_root().map_err(|source| {
InstallRootError::new(InstallRootPhase::IcpProjectDiscovery, source)
})?,
};
Ok((workspace_root, icp_root))
}
struct CurrentFleetInstallPlanRequest<'a> {
icp_root: &'a Path,
environment: &'a str,
fleet_name: &'a str,
app_id: &'a str,
config_path: &'a Path,
finalized_release_build: &'a crate::release_build::FinalizedReleaseBuild,
input: ResolvedFleetInstallInput,
fresh_fleet_plan: &'a FreshFleetDeploymentPlanV1,
}
fn plan_current_fleet_install(
request: CurrentFleetInstallPlanRequest<'_>,
) -> Result<PlannedCurrentFleetInstall, InstallRootError> {
let session = plan_current_fleet_install_session(
request.icp_root,
request.environment,
request.fleet_name,
request.app_id,
request.finalized_release_build,
request.fresh_fleet_plan,
)?;
let plan = persist_current_fleet_install_plan(
request.icp_root,
request.config_path,
request.fleet_name,
session.fleet.clone(),
request.finalized_release_build,
request.input,
request.fresh_fleet_plan,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
Ok(PlannedCurrentFleetInstall { session, plan })
}
struct PlannedCurrentFleetInstall {
session: fleet_install_session::FleetInstallSession,
plan: PersistedFleetInstallPlan,
}
impl PlannedCurrentFleetInstall {
const fn fleet(&self) -> canic_core::ids::FleetKey {
self.session.fleet.fleet
}
}
fn require_finalized_release_build(
finalized: Option<crate::release_build::FinalizedReleaseBuild>,
) -> Result<crate::release_build::FinalizedReleaseBuild, InstallRootError> {
finalized.ok_or_else(|| {
InstallRootError::new(
InstallRootPhase::Manifest,
ReleaseBuildPlanError::MissingFinalizedAuthority,
)
})
}
fn require_recompiled_fresh_fleet_plan(
announced: &FreshFleetDeploymentPlanV1,
recompiled: &FreshFleetDeploymentPlanV1,
) -> Result<(), InstallRootError> {
require_fresh_fleet_plan_digest(Some(&announced.plan_digest), &recompiled.plan_digest)
.map_err(|source| InstallRootError::new(InstallRootPhase::Planning, source))
}
fn require_install_app_unchanged(
preflight_app_id: &str,
install_snapshot: &build_snapshot::ValidatedInstallSnapshot,
) -> Result<(), InstallRootError> {
if install_snapshot.app_id == preflight_app_id {
return Ok(());
}
Err(InstallRootError::new(
InstallRootPhase::Identity,
InstallAppSnapshotChangedError {
preflight: preflight_app_id.to_string(),
build_snapshot: install_snapshot.app_id.clone(),
},
))
}
fn resolve_current_fleet_install_input(
icp_root: &Path,
environment: &str,
options: &InstallRootOptions,
catalog_acquisition: FleetCatalogAcquisition,
) -> Result<ResolvedFleetInstallInput, Box<dyn std::error::Error>> {
let input_path = current_fleet_install_input_path(icp_root, options)?;
match catalog_acquisition {
FleetCatalogAcquisition::CacheOnly => {
load_and_resolve_fleet_install_input_for_preflight(icp_root, environment, &input_path)
}
FleetCatalogAcquisition::RefreshMissingOrInvalid => {
load_and_resolve_fleet_install_input(icp_root, environment, &input_path)
}
}
.map_err(Into::into)
}
fn current_fleet_install_input_path(
icp_root: &Path,
options: &InstallRootOptions,
) -> Result<PathBuf, MissingFleetInstallInputError> {
let input_path = options
.fleet_install_input_path
.as_ref()
.ok_or(MissingFleetInstallInputError)?;
Ok(if input_path.is_absolute() {
input_path.clone()
} else {
icp_root.join(input_path)
})
}
fn compile_current_fresh_fleet_preflight(
config: &AppConfigSnapshot,
app_id: &str,
fleet_name: &str,
input: &ResolvedFleetInstallInput,
build_profile: crate::canister_build::CanisterBuildProfile,
release_build_id: Option<canic_core::ids::ReleaseBuildId>,
) -> Result<FreshFleetPreflightV1, Box<dyn std::error::Error>> {
let fleet_name = fleet_name.parse()?;
compile_fresh_fleet_preflight(FreshFleetPreflightRequest {
config: config.model(),
app: app_id,
fleet_name: &fleet_name,
coordinator: &input.coordinator,
fleet_subnet_roots: &input.fleet_subnet_roots,
build_profile,
release_build_id,
effects: FreshFleetPreflightEffectsV1::none_started(),
})
.map_err(Into::into)
}
type CurrentInstallPreflightReleaseSource = (
crate::canister_build::CanisterBuildProfile,
Option<canic_core::ids::ReleaseBuildId>,
Option<String>,
);
fn current_install_preflight_release_source(
icp_root: &Path,
canonical_network_id: canic_core::ids::CanonicalNetworkId,
fleet_name: &str,
app_id: &str,
options: &InstallRootOptions,
) -> Result<CurrentInstallPreflightReleaseSource, Box<dyn std::error::Error>> {
let fleet_name = fleet_name.parse()?;
let app = app_id.into();
if let Some(recovered) = fleet_install_session::recover_fleet_install_session_authority(
icp_root,
canonical_network_id,
&fleet_name,
&app,
)? {
let finalized = recovered.finalized_release_build;
if options
.release_build_id
.is_some_and(|requested| requested != finalized.record.release_build_id)
{
return Err(
"requested release build differs from the interrupted Fleet install session".into(),
);
}
require_current_release_builder(&finalized.record.builder_version)?;
require_requested_build_profile(options.build_profile, finalized.record.build_profile)?;
return Ok((
finalized.record.build_profile,
recovered.decision_release_build_id,
Some(recovered.fresh_fleet_plan_digest),
));
}
if let Some(release_build_id) = options.release_build_id {
let finalized = load_finalized_release_build(icp_root, release_build_id)?;
require_current_release_builder(&finalized.record.builder_version)?;
require_requested_build_profile(options.build_profile, finalized.record.build_profile)?;
return Ok((finalized.record.build_profile, Some(release_build_id), None));
}
Ok((
options
.build_profile
.unwrap_or(crate::canister_build::CanisterBuildProfile::Release),
None,
None,
))
}
fn persist_current_fleet_install_plan(
icp_root: &Path,
config_path: &Path,
fleet_name: &str,
fleet: canic_core::ids::FleetBinding,
finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
input: ResolvedFleetInstallInput,
fresh_fleet_plan: &FreshFleetDeploymentPlanV1,
) -> Result<PersistedFleetInstallPlan, Box<dyn std::error::Error>> {
let config = AppConfigSnapshot::load(config_path)?;
compile_and_persist_fleet_install_plan(FleetInstallPlanRequest {
root: icp_root,
config: config.model(),
fleet,
fleet_name: fleet_name.parse()?,
fresh_fleet_plan_digest: fresh_fleet_plan.plan_digest.clone(),
release_build_id: finalized_release_build.record.release_build_id,
coordinator: input.coordinator,
fleet_subnet_roots: input.fleet_subnet_roots,
})
.map_err(Into::into)
}
fn current_install_config_path(
icp_root: &Path,
options: &InstallRootOptions,
) -> Result<PathBuf, InstallRootError> {
resolve_install_config_path(
icp_root,
options.config_path.as_deref(),
options.interactive_config_selection,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))
}
fn plan_current_fleet_install_session(
icp_root: &Path,
environment: &str,
fleet_name: &str,
app_id: &str,
finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
fresh_fleet_plan: &FreshFleetDeploymentPlanV1,
) -> Result<fleet_install_session::FleetInstallSession, InstallRootError> {
let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)
.map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
let fleet_name = fleet_name
.parse()
.map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
fleet_install_session::plan_fleet_install_session(
fleet_install_session::PlanFleetInstallSessionRequest {
root: icp_root,
canonical_network_id,
fleet_name,
app: app_id.into(),
finalized_release_build,
decision_release_build_id: fresh_fleet_plan.preflight.release_build_id,
fresh_fleet_plan_digest: &fresh_fleet_plan.plan_digest,
},
)
.map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))
}
fn require_fresh_fleet_plan_digest(
expected: Option<&str>,
observed: &str,
) -> Result<(), FreshFleetPlanDigestMismatchError> {
match expected {
Some(expected) if expected != observed => Err(FreshFleetPlanDigestMismatchError {
expected: expected.to_string(),
observed: observed.to_string(),
}),
_ => Ok(()),
}
}
fn print_fresh_fleet_decision(plan: &FreshFleetDeploymentPlanV1) {
let maximum_debit = match &plan.maximum_operator_debit {
PlannedCanisterCreationFunding::Cycles { cycles } => format!("{cycles} cycles"),
PlannedCanisterCreationFunding::Icp { e8s } => format!("{e8s} ICP e8s"),
};
TerminalStyle::detected().print_section(
"Fresh-Fleet decision",
&format!(
"plan {} with maximum operator debit {maximum_debit}",
plan.plan_digest
),
);
print_fresh_fleet_placement_warnings(plan);
println!();
}
fn print_fresh_fleet_placement_warnings(plan: &FreshFleetDeploymentPlanV1) {
let style = TerminalStyle::detected();
if let Some(warning) = plan.preflight.coordinator.placement_cost.warning.as_deref() {
style.print_section("Fiduciary placement warning", warning);
}
for root in &plan.preflight.fleet_subnet_roots {
if let Some(warning) = root.placement_cost.warning.as_deref() {
style.print_section("Fiduciary placement warning", warning);
}
}
}
fn print_paid_effect_placement_warnings(plan: &crate::fleet_install_plan::FleetInstallPlan) {
let style = TerminalStyle::detected();
if let Some(warning) = plan.coordinator.placement_cost.warning.as_deref() {
style.print_section("Fiduciary paid-effect warning", warning);
}
for root in &plan.fleet_subnet_roots {
if let Some(warning) = root.placement_cost.warning.as_deref() {
style.print_section("Fiduciary paid-effect warning", warning);
}
}
if plan.coordinator.placement_cost.warning.is_some()
|| plan
.fleet_subnet_roots
.iter()
.any(|root| root.placement_cost.warning.is_some())
{
println!();
}
}
fn print_install_identity(app: &str, fleet_name: &str) {
TerminalStyle::detected().print_section(
&format!("Install Fleet {fleet_name}"),
&format!("source App {app}"),
);
println!();
}
fn persist_current_pre_root_receipts(
receipt_scope: InstallReceiptScope<'_>,
prepared_receipts: &[DeploymentReceiptV1],
build_phase: CompletedInstallPhase,
manifest_phase: CompletedInstallPhase,
) -> Result<(), InstallRootError> {
persist_pre_root_receipts(
receipt_scope,
prepared_receipts,
build_phase,
manifest_phase,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
}
fn install_current_fleet_coordinator(
icp_context: &InstallIcpContext,
config_path: &Path,
plan: &PersistedFleetInstallPlan,
) -> Result<(coordinator_install::VerifiedFleetCoordinator, Duration), InstallRootError> {
let started = Instant::now();
let coordinator = install_and_verify_fleet_coordinator(icp_context, config_path, plan)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
Ok((coordinator, started.elapsed()))
}
fn install_current_fleet_subnet_roots(
icp_context: &InstallIcpContext,
config_path: &Path,
planned: &PlannedCurrentFleetInstall,
coordinator: canic_core::cdk::types::Principal,
) -> Result<Duration, InstallRootError> {
let started = Instant::now();
install_and_verify_fleet_subnet_roots(
icp_context,
config_path,
&planned.plan,
coordinator,
planned.session.operation_id,
)
.map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
Ok(started.elapsed())
}
fn persist_pre_root_receipts(
receipt_scope: InstallReceiptScope<'_>,
prepared_receipts: &[DeploymentReceiptV1],
build_phase: CompletedInstallPhase,
manifest_phase: CompletedInstallPhase,
) -> Result<(), Box<dyn std::error::Error>> {
let mut paths = Vec::with_capacity(prepared_receipts.len() + 2);
for receipt in prepared_receipts {
paths.push(receipt_scope.write_receipt(receipt)?);
}
paths.push(write_completed_install_phase_receipt(
receipt_scope,
build_phase,
)?);
paths.push(write_completed_install_phase_receipt(
receipt_scope,
manifest_phase,
)?);
let receipt_root = paths.first().and_then(|path| path.parent()).map_or_else(
|| "deployment receipt directory".to_string(),
|path| path.display().to_string(),
);
TerminalStyle::detected().print_section(
"Receipts",
&format!("{} written to {receipt_root}", paths.len()),
);
println!();
Ok(())
}
fn current_install_build_inputs(
workspace_root: &std::path::Path,
icp_root: &std::path::Path,
config_path: &std::path::Path,
icp: &InstallIcpContext,
options: &InstallRootOptions,
) -> Result<
(
crate::canister_build::WorkspaceBuildContext,
build_snapshot::ValidatedInstallSnapshot,
),
Box<dyn std::error::Error>,
> {
let mut context = resolve_install_build_context(
workspace_root,
config_path,
icp,
&options.root_build_target,
options.build_profile,
)?;
if options.deployment_plan_override.is_some() {
let snapshot = resolve_install_snapshot(&context, &options.root_build_target, true)?;
return Ok((context, snapshot));
}
let config = AppConfigSnapshot::load(config_path)?;
let release_build = current_install_release_build(
icp_root,
&options.environment,
&options.fleet_name,
config.app_id(),
options.release_build_id,
options.build_profile,
)?;
context = context
.with_profile(release_build.record.build_profile)
.with_release_build_id(release_build.record.release_build_id);
let mut snapshot = resolve_install_snapshot(&context, &options.root_build_target, false)?;
snapshot.release_build = Some(release_build);
Ok((context, snapshot))
}
fn current_install_release_build(
icp_root: &Path,
environment: &str,
fleet_name: &str,
app_id: &str,
requested_release_build_id: Option<canic_core::ids::ReleaseBuildId>,
requested_build_profile: Option<crate::canister_build::CanisterBuildProfile>,
) -> Result<PlannedReleaseBuild, Box<dyn std::error::Error>> {
let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)?;
let fleet_name = fleet_name.parse()?;
let app = app_id.into();
if let Some(finalized) = fleet_install_session::recover_fleet_install_session_release_build(
icp_root,
canonical_network_id,
&fleet_name,
&app,
)? {
if requested_release_build_id
.is_some_and(|requested| requested != finalized.record.release_build_id)
{
return Err(
"requested release build differs from the interrupted Fleet install session".into(),
);
}
require_current_release_builder(&finalized.record.builder_version)?;
require_requested_build_profile(requested_build_profile, finalized.record.build_profile)?;
return Ok(PlannedReleaseBuild {
record: finalized.record,
path: finalized.path,
});
}
if let Some(release_build_id) = requested_release_build_id {
let finalized = load_finalized_release_build(icp_root, release_build_id)?;
require_current_release_builder(&finalized.record.builder_version)?;
require_requested_build_profile(requested_build_profile, finalized.record.build_profile)?;
return Ok(PlannedReleaseBuild {
record: finalized.record,
path: finalized.path,
});
}
plan_release_build_for_profile(
icp_root,
requested_build_profile.unwrap_or(crate::canister_build::CanisterBuildProfile::Release),
)
.map_err(Into::into)
}
fn require_current_release_builder(recorded: &str) -> Result<(), Box<dyn std::error::Error>> {
if recorded != env!("CARGO_PKG_VERSION") {
return Err(format!(
"finalized release build belongs to Canic {recorded}, not current Canic {}",
env!("CARGO_PKG_VERSION")
)
.into());
}
Ok(())
}
fn require_requested_build_profile(
requested: Option<crate::canister_build::CanisterBuildProfile>,
recorded: crate::canister_build::CanisterBuildProfile,
) -> Result<(), Box<dyn std::error::Error>> {
if requested.is_some_and(|requested| requested != recorded) {
return Err(format!(
"requested build profile differs from finalized release build profile {}",
recorded.target_dir_name()
)
.into());
}
Ok(())
}