use super::fleet_subnet_root_install_journal::{
FleetSubnetRootInstallPhase, PlanFleetSubnetRootInstallRequest, ResolvedFleetSubnetRootInstall,
begin_root_activation, begin_root_activation_preparation, plan_fleet_subnet_root_install,
record_root_activated, record_root_activation_prepared, record_root_activation_verified,
validate_live_root_activation_status,
};
use crate::{
fleet_install_plan::PersistedFleetInstallPlan,
icp::{IcpCli, LocalReplicaTarget, decode_json_result_response},
release_set::{AppConfigSnapshot, load_persisted_canic_infrastructure_artifact_manifest},
};
use candid::{CandidType, IDLValue, Principal};
use canic_core::{
dto::{
component_registry::{
RootComponentRegistryPreparationRequest, RootComponentRegistryStatusResponse,
},
fleet_activation::{FleetActivationResumeRequest, FleetActivationStatusResponse},
},
protocol,
};
use std::path::Path;
use thiserror::Error as ThisError;
const ICP_JSON_OUTPUT: &str = "json";
const MAX_ROOT_ACTIVATION_TRANSITIONS: usize = 7;
#[derive(Debug, ThisError)]
enum RootRuntimeActivationError {
#[error("root runtime activation reached unexpected phase {0:?}")]
UnexpectedPhase(FleetSubnetRootInstallPhase),
#[error("root runtime activation exceeded its bounded phase transitions")]
TransitionBoundExceeded,
#[error("live root runtime or Component Registry differs from durable activation evidence")]
LiveEvidenceMismatch,
}
pub(super) struct ActivateFleetSubnetRootRuntimesRequest<'a> {
pub icp_root: &'a Path,
pub environment: &'a str,
pub local_replica: Option<&'a LocalReplicaTarget>,
pub config_path: &'a Path,
pub fleet_install_plan: &'a PersistedFleetInstallPlan,
pub coordinator: Principal,
pub install_operation_id: [u8; 32],
}
pub(super) fn activate_and_verify_fleet_subnet_root_runtimes(
request: ActivateFleetSubnetRootRuntimesRequest<'_>,
) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfigSnapshot::load(request.config_path)?;
let component_topology = config.model().compile_component_topology()?;
let infrastructure_manifest = load_persisted_canic_infrastructure_artifact_manifest(
request.icp_root,
request.fleet_install_plan.plan.release_build_id,
)?;
for root_plan in &request.fleet_install_plan.plan.fleet_subnet_roots {
let current = plan_fleet_subnet_root_install(PlanFleetSubnetRootInstallRequest {
fleet_install_plan: request.fleet_install_plan,
infrastructure_manifest: &infrastructure_manifest,
coordinator: request.coordinator,
install_operation_id: request.install_operation_id,
component_topology: component_topology.clone(),
root_plan,
})?;
let component_registry_request = current
.journal
.component_registry_preparation_request
.clone()
.ok_or(RootRuntimeActivationError::LiveEvidenceMismatch)?;
drive_root_runtime_activation(
request.icp_root,
request.environment,
request.local_replica,
current,
component_registry_request,
)?;
}
Ok(())
}
fn drive_root_runtime_activation(
icp_root: &Path,
environment: &str,
local_replica: Option<&LocalReplicaTarget>,
mut current: ResolvedFleetSubnetRootInstall,
component_registry_request: RootComponentRegistryPreparationRequest,
) -> Result<(), Box<dyn std::error::Error>> {
let root = current
.journal
.fleet_subnet_root
.ok_or(RootRuntimeActivationError::LiveEvidenceMismatch)?;
let icp = root_icp(icp_root, environment, local_replica);
for _ in 0..MAX_ROOT_ACTIVATION_TRANSITIONS {
current = match current.journal.phase {
FleetSubnetRootInstallPhase::ComponentRegistryPreparationVerified => {
begin_root_activation_preparation(¤t)?
}
FleetSubnetRootInstallPhase::RootActivationPreparationInFlight => {
let observed: FleetActivationStatusResponse =
query_no_arg(&icp, root, protocol::CANIC_FLEET_ACTIVATION_STATUS)?;
validate_live_root_activation_status(¤t.path, ¤t.journal, &observed)?;
let response = call_no_arg(&icp, root, protocol::CANIC_PREPARE_FLEET_ACTIVATION)?;
record_root_activation_prepared(¤t, response)?
}
FleetSubnetRootInstallPhase::RootActivationPrepared => begin_root_activation(¤t)?,
FleetSubnetRootInstallPhase::RootActivationInFlight => {
let prepared = current
.journal
.root_activation_preparation_response
.as_ref()
.ok_or(RootRuntimeActivationError::LiveEvidenceMismatch)?;
let request = FleetActivationResumeRequest {
operation_id: current.journal.install_operation_id,
credential: prepared
.credential
.ok_or(RootRuntimeActivationError::LiveEvidenceMismatch)?,
};
let observed: FleetActivationStatusResponse =
query_no_arg(&icp, root, protocol::CANIC_FLEET_ACTIVATION_STATUS)?;
validate_live_root_activation_status(¤t.path, ¤t.journal, &observed)?;
let response = call_with_arg(
&icp,
root,
protocol::CANIC_RESUME_FLEET_ACTIVATION,
request,
false,
)?;
record_root_activated(¤t, response)?
}
FleetSubnetRootInstallPhase::RootActivated => {
let response = query_no_arg(&icp, root, protocol::CANIC_FLEET_ACTIVATION_STATUS)?;
let component_registry = call_with_arg(
&icp,
root,
protocol::CANIC_ROOT_COMPONENT_REGISTRY_STATUS,
component_registry_request.clone(),
true,
)?;
record_root_activation_verified(¤t, response, component_registry)?
}
FleetSubnetRootInstallPhase::RootActivationVerified => {
let response: FleetActivationStatusResponse =
query_no_arg(&icp, root, protocol::CANIC_FLEET_ACTIVATION_STATUS)?;
let component_registry: RootComponentRegistryStatusResponse = call_with_arg(
&icp,
root,
protocol::CANIC_ROOT_COMPONENT_REGISTRY_STATUS,
component_registry_request,
true,
)?;
if current.journal.root_activation_response.as_ref() != Some(&response)
|| current
.journal
.component_registry_activation_response
.as_ref()
!= Some(&component_registry)
{
return Err(RootRuntimeActivationError::LiveEvidenceMismatch.into());
}
return Ok(());
}
phase => return Err(RootRuntimeActivationError::UnexpectedPhase(phase).into()),
};
}
Err(RootRuntimeActivationError::TransitionBoundExceeded.into())
}
fn call_no_arg<O>(
icp: &IcpCli,
canister: Principal,
method: &str,
) -> Result<O, Box<dyn std::error::Error>>
where
O: CandidType + serde::de::DeserializeOwned,
{
let output = icp.canister_call_arg_output_with_candid(
&canister.to_text(),
method,
"()",
Some(ICP_JSON_OUTPUT),
None,
)?;
decode_json_result_response(&output).map_err(Into::into)
}
fn call_with_arg<I, O>(
icp: &IcpCli,
canister: Principal,
method: &str,
input: I,
query: bool,
) -> Result<O, Box<dyn std::error::Error>>
where
I: CandidType,
O: CandidType + serde::de::DeserializeOwned,
{
let value = IDLValue::try_from_candid_type(&input)?;
let args = format!("({value})");
let output = if query {
icp.canister_query_arg_output_with_candid(
&canister.to_text(),
method,
&args,
Some(ICP_JSON_OUTPUT),
None,
)?
} else {
icp.canister_call_arg_output_with_candid(
&canister.to_text(),
method,
&args,
Some(ICP_JSON_OUTPUT),
None,
)?
};
decode_json_result_response(&output).map_err(Into::into)
}
fn query_no_arg<O>(
icp: &IcpCli,
canister: Principal,
method: &str,
) -> Result<O, Box<dyn std::error::Error>>
where
O: CandidType + serde::de::DeserializeOwned,
{
let output = icp.canister_query_output_with_candid(
&canister.to_text(),
method,
Some(ICP_JSON_OUTPUT),
None,
)?;
decode_json_result_response(&output).map_err(Into::into)
}
fn root_icp(
icp_root: &Path,
environment: &str,
local_replica: Option<&LocalReplicaTarget>,
) -> IcpCli {
IcpCli::new("icp", Some(environment.to_string()))
.with_cwd(icp_root)
.with_local_replica(local_replica.cloned())
}