use super::fleet_activation_journal::{
ActivatedFleetActivationEvidence, CanistersActivatedFleetInstallActivation,
CanistersPreparedFleetInstallActivation, FleetInstallActivationJournalError,
FleetInstallActivationPhase, ResolvedFleetInstallActivation, admit_canisters_activated,
admit_canisters_prepared, admit_root_install_receipt, canisters_prepared_resume_request,
record_canisters_activated, record_canisters_prepared, record_root_installed,
recover_root_install_receipt, resume_canisters_activated, resume_canisters_prepared,
};
use super::operations::InstallRootWasmOperation;
use super::options::InstallRootOptions;
use super::phase_receipts::InstallReceiptScope;
use super::plan_artifacts::{PreparedPlanArtifacts, normal_install_root_wasm};
use super::receipt_io::install_deployment_truth_receipts_dir;
use super::timing::InstallTimingSummary;
use crate::{
canister_build::WorkspaceBuildContext,
icp::{IcpCli, decode_json_result_response},
};
use candid::IDLValue;
use canic_core::{
dto::fleet_activation::{FleetActivationResumeRequest, FleetActivationStatusResponse},
protocol,
};
use std::path::Path;
use thiserror::Error as ThisError;
pub(super) struct ActivatedRootInstall {
pub(super) timings: InstallTimingSummary,
pub(super) activation: CanistersActivatedFleetInstallActivation,
}
#[derive(Debug, ThisError)]
#[error(
"root Fleet activation {operation_name} failed and its exact status could not be reconciled: operation={operation}; reconciliation={reconciliation}"
)]
struct FleetActivationCallReconciliationError {
operation_name: &'static str,
#[source]
operation: Box<dyn std::error::Error>,
reconciliation: Box<dyn std::error::Error>,
}
pub(super) fn install_root_activated(
receipt_scope: InstallReceiptScope<'_>,
options: &InstallRootOptions,
root_canister_id: &str,
build_context: &WorkspaceBuildContext,
plan_artifacts: Option<&PreparedPlanArtifacts>,
activation: &ResolvedFleetInstallActivation,
) -> Result<ActivatedRootInstall, Box<dyn std::error::Error>> {
if activation.journal.phase == FleetInstallActivationPhase::CanistersActivated {
return Ok(ActivatedRootInstall {
timings: InstallTimingSummary::default(),
activation: resume_canisters_activated(activation)?,
});
}
let prepared = install_root_prepared(
receipt_scope,
options,
root_canister_id,
build_context,
plan_artifacts,
activation,
)?;
let root_canister = root_canister_id.parse()?;
let evidence = resume_and_admit_activation(
receipt_scope.icp_root,
receipt_scope.environment,
root_canister_id,
build_context,
root_canister,
&prepared.activation,
)?;
let activation =
record_canisters_activated(receipt_scope.icp_root, &prepared.activation, &evidence)?;
Ok(ActivatedRootInstall {
timings: prepared.timings,
activation,
})
}
fn install_root_prepared(
receipt_scope: InstallReceiptScope<'_>,
options: &InstallRootOptions,
root_canister_id: &str,
build_context: &WorkspaceBuildContext,
plan_artifacts: Option<&PreparedPlanArtifacts>,
activation: &ResolvedFleetInstallActivation,
) -> Result<PreparedRootInstall, Box<dyn std::error::Error>> {
let mut timings = InstallTimingSummary::default();
if activation.journal.phase == FleetInstallActivationPhase::CanistersPrepared {
return Ok(PreparedRootInstall {
timings,
activation: resume_canisters_prepared(activation)?,
});
}
let resolved_root = root_canister_id.parse()?;
let receipt = match activation.journal.phase {
FleetInstallActivationPhase::Planned => {
let root_wasm = match plan_artifacts {
Some(artifacts) => artifacts.verified_root_wasm_path()?,
None => {
normal_install_root_wasm(receipt_scope.icp_root, &options.root_build_target)
}
};
let install_operation = InstallRootWasmOperation::new(
receipt_scope.icp_root,
receipt_scope.environment,
root_canister_id,
root_wasm,
&activation.journal.activation.identity,
build_context.local_replica.as_ref(),
)?;
let completed_root_install = receipt_scope
.run_operation_with_receipt(&install_operation, Some(root_canister_id))?;
timings.install_root = completed_root_install.duration;
admit_root_install_receipt(&completed_root_install.receipt_path)?
}
FleetInstallActivationPhase::RootInstalled => recover_root_install_receipt(
&install_deployment_truth_receipts_dir(receipt_scope.icp_root, receipt_scope.fleet),
activation
.journal
.root_install_receipt_hash
.expect("validated RootInstalled journal retains its receipt hash"),
)?,
phase => {
return Err(Box::new(
FleetInstallActivationJournalError::InvalidCanistersPreparedTransition { phase },
));
}
};
if receipt.root_canister != resolved_root {
return Err(Box::new(
FleetInstallActivationJournalError::RootInstallReceiptCanisterMismatch {
receipt_root: receipt.root_canister,
resolved_root,
},
));
}
let root_installed = record_root_installed(receipt_scope.icp_root, activation, &receipt)?;
let icp = IcpCli::new("icp", Some(receipt_scope.environment.to_string()))
.with_cwd(receipt_scope.icp_root)
.with_local_replica(build_context.local_replica.clone());
let evidence = match call_prepare(&icp, root_canister_id) {
Ok(status) => admit_canisters_prepared(
receipt.root_canister,
&root_installed.journal.activation.identity,
&status,
)?,
Err(operation) => {
let reconciled = query_status(&icp, root_canister_id).and_then(|status| {
admit_canisters_prepared(
receipt.root_canister,
&root_installed.journal.activation.identity,
&status,
)
.map_err(Into::into)
});
match reconciled {
Ok(evidence) => evidence,
Err(reconciliation) => {
return Err(Box::new(FleetActivationCallReconciliationError {
operation_name: "preparation",
operation,
reconciliation,
}));
}
}
}
};
let activation = record_canisters_prepared(receipt_scope.icp_root, &root_installed, &evidence)?;
Ok(PreparedRootInstall {
timings,
activation,
})
}
struct PreparedRootInstall {
timings: InstallTimingSummary,
activation: CanistersPreparedFleetInstallActivation,
}
fn resume_and_admit_activation(
icp_root: &Path,
environment: &str,
root_canister_id: &str,
build_context: &WorkspaceBuildContext,
root_canister: canic_core::cdk::types::Principal,
prepared: &CanistersPreparedFleetInstallActivation,
) -> Result<ActivatedFleetActivationEvidence, Box<dyn std::error::Error>> {
let icp = IcpCli::new("icp", Some(environment.to_string()))
.with_cwd(icp_root)
.with_local_replica(build_context.local_replica.clone());
let request = canisters_prepared_resume_request(prepared);
match call_resume(&icp, root_canister_id, &request) {
Ok(status) => {
admit_canisters_activated(root_canister, prepared, &status).map_err(Into::into)
}
Err(operation) => {
let reconciled = query_status(&icp, root_canister_id).and_then(|status| {
admit_canisters_activated(root_canister, prepared, &status).map_err(Into::into)
});
match reconciled {
Ok(evidence) => Ok(evidence),
Err(reconciliation) => Err(Box::new(FleetActivationCallReconciliationError {
operation_name: "resume",
operation,
reconciliation,
})),
}
}
}
}
fn call_prepare(
icp: &IcpCli,
root_canister_id: &str,
) -> Result<FleetActivationStatusResponse, Box<dyn std::error::Error>> {
let output = icp.canister_call_arg_output_with_candid(
root_canister_id,
protocol::CANIC_PREPARE_FLEET_ACTIVATION,
"()",
Some("json"),
None,
)?;
decode_json_result_response(&output).map_err(Into::into)
}
fn call_resume(
icp: &IcpCli,
root_canister_id: &str,
request: &FleetActivationResumeRequest,
) -> Result<FleetActivationStatusResponse, Box<dyn std::error::Error>> {
let output = icp.canister_call_arg_output_with_candid(
root_canister_id,
protocol::CANIC_RESUME_FLEET_ACTIVATION,
&fleet_activation_resume_args(request)?,
Some("json"),
None,
)?;
decode_json_result_response(&output).map_err(Into::into)
}
fn fleet_activation_resume_args(
request: &FleetActivationResumeRequest,
) -> Result<String, candid::Error> {
let value = IDLValue::try_from_candid_type(request)?;
Ok(format!("({value})"))
}
fn query_status(
icp: &IcpCli,
root_canister_id: &str,
) -> Result<FleetActivationStatusResponse, Box<dyn std::error::Error>> {
let output = icp.canister_query_output_with_candid(
root_canister_id,
protocol::CANIC_FLEET_ACTIVATION_STATUS,
Some("json"),
None,
)?;
decode_json_result_response(&output).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
use candid::{CandidType, TypeEnv};
use canic_core::dto::fleet_activation::FleetCredentialGenerationRef;
#[test]
fn resume_args_roundtrip_the_exact_journal_request() {
let request = FleetActivationResumeRequest {
operation_id: [7; 32],
credential: FleetCredentialGenerationRef {
generation: 8,
manifest_hash: [9; 32],
},
};
let args = fleet_activation_resume_args(&request).expect("build resume args");
let parsed = candid_parser::parse_idl_args(&args).expect("parse textual Candid");
let bytes = parsed
.to_bytes_with_types(&TypeEnv::new(), &[FleetActivationResumeRequest::ty()])
.expect("encode typed textual Candid");
let decoded: FleetActivationResumeRequest =
candid::decode_one(&bytes).expect("decode resume request");
assert_eq!(decoded, request);
}
}