Skip to main content

empty_fleet_subnet_root_retirement/
empty_fleet_subnet_root_retirement.rs

1use candid::{CandidType, Principal};
2use canic_control_plane::dto::fleet_coordinator::{
3    CoordinatorCommand, CoordinatorCommandResponse, CoordinatorStatusRequest,
4    CoordinatorStatusResponse,
5};
6use canic_core::{
7    cdk::utils::hash::{decode_hex, hex_bytes},
8    dto::{
9        fleet_registry::{FleetRegistry, FleetSubnetRootDrainingReservationRequest},
10        role::OperationReceipt,
11    },
12    protocol,
13};
14use canic_host::{
15    fleet_subnet_root_deletion::{
16        FleetSubnetRootDeletionHostRequest, execute_fleet_subnet_root_deletion,
17        prepare_fleet_subnet_root_deletion_execution,
18    },
19    icp::{IcpCli, decode_json_result_response},
20    protocol_binding::{ResolvedProtocolBinding, resolve_infrastructure_protocol_binding},
21    release_set::{CanicInfrastructureRole, load_persisted_canic_infrastructure_artifact_manifest},
22};
23use serde::de::DeserializeOwned;
24use std::{env, fs, path::PathBuf};
25
26const USAGE: &str = "usage: cargo run -p canic-host --example empty_fleet_subnet_root_retirement -- \
27    --confirm-disposable-empty-root <icp-executable> <icp-root> <environment> \
28    <release-build-id> <coordinator-principal> <fleet-subnet-root-principal> <operation-id-hex>";
29
30struct RetirementContext {
31    icp_executable: String,
32    icp_root: PathBuf,
33    environment: String,
34    release_build_id: canic_core::ids::ReleaseBuildId,
35    coordinator: Principal,
36    fleet_subnet_root: Principal,
37    operation_id: [u8; 32],
38}
39
40fn main() -> Result<(), Box<dyn std::error::Error>> {
41    let context = parse_context()?;
42    let icp = IcpCli::new(
43        context.icp_executable.clone(),
44        Some(context.environment.clone()),
45    )
46    .with_cwd(&context.icp_root);
47    let manifest = load_persisted_canic_infrastructure_artifact_manifest(
48        &context.icp_root,
49        context.release_build_id,
50    )?;
51    let artifact = |role| {
52        manifest
53            .manifest
54            .entries
55            .iter()
56            .find(|entry| entry.role == role)
57            .ok_or("infrastructure artifact role is missing")
58    };
59    let coordinator_protocol_binding = resolve_infrastructure_protocol_binding(
60        &context.icp_root,
61        &context.environment,
62        artifact(CanicInfrastructureRole::FleetCoordinator)?,
63    )?;
64    let root_protocol_binding = resolve_infrastructure_protocol_binding(
65        &context.icp_root,
66        &context.environment,
67        artifact(CanicInfrastructureRole::FleetSubnetRoot)?,
68    )?;
69    let registry = coordinator_registry(&icp, &coordinator_protocol_binding, context.coordinator)?;
70    let expected_root = registry
71        .fleet_subnet_roots
72        .iter()
73        .find(|entry| entry.fleet_subnet_root == context.fleet_subnet_root)
74        .cloned()
75        .ok_or("Fleet Subnet Root is absent from the Coordinator Registry")?;
76    let CoordinatorStatusResponse::RegistryVersion(expected_registry) = query(
77        &icp,
78        &coordinator_protocol_binding,
79        context.coordinator,
80        protocol::CANIC_STATUS,
81        &CoordinatorStatusRequest::RegistryVersion,
82    )?
83    else {
84        return Err("Coordinator returned a differently correlated status response".into());
85    };
86    let response: CoordinatorCommandResponse = call(
87        &icp,
88        &coordinator_protocol_binding,
89        context.coordinator,
90        protocol::CANIC_COMMAND,
91        &CoordinatorCommand::RemoveRoot(FleetSubnetRootDrainingReservationRequest {
92            operation_id: context.operation_id,
93            expected_registry,
94            expected_root,
95        }),
96    )?;
97    let expected_receipt = OperationReceipt {
98        operation_id: context.operation_id,
99    };
100    match response {
101        CoordinatorCommandResponse::OperationAccepted(receipt) if receipt == expected_receipt => {}
102        _ => return Err("Coordinator returned a differently correlated removal response".into()),
103    }
104
105    let host_request = || FleetSubnetRootDeletionHostRequest {
106        icp_executable: &context.icp_executable,
107        icp_root: &context.icp_root,
108        environment: &context.environment,
109        local_replica: None,
110        coordinator: context.coordinator,
111        coordinator_protocol_binding: &coordinator_protocol_binding,
112        fleet_subnet_root: context.fleet_subnet_root,
113        root_protocol_binding: &root_protocol_binding,
114        operation_id: context.operation_id,
115    };
116    let execution = prepare_fleet_subnet_root_deletion_execution(host_request())?;
117    let terminal = execute_fleet_subnet_root_deletion(host_request())?;
118    println!(
119        "removed Fleet Subnet Root {} with operation 0x{}; execution 0x{} completed at {}",
120        terminal.fleet_subnet_root,
121        hex_bytes(terminal.operation_id),
122        hex_bytes(execution.execution_hash),
123        terminal.completed_at_ns,
124    );
125    Ok(())
126}
127
128fn parse_context() -> Result<RetirementContext, Box<dyn std::error::Error>> {
129    let mut args = env::args().skip(1);
130    let (
131        Some(confirmation),
132        Some(icp_executable),
133        Some(icp_root),
134        Some(environment),
135        Some(release_build_id),
136        Some(coordinator),
137        Some(fleet_subnet_root),
138        Some(operation_id_hex),
139    ) = (
140        args.next(),
141        args.next(),
142        args.next(),
143        args.next(),
144        args.next(),
145        args.next(),
146        args.next(),
147        args.next(),
148    )
149    else {
150        return Err(USAGE.into());
151    };
152    if confirmation != "--confirm-disposable-empty-root" || args.next().is_some() {
153        return Err(USAGE.into());
154    }
155    let operation_id = decode_hex(
156        operation_id_hex
157            .strip_prefix("0x")
158            .unwrap_or(&operation_id_hex),
159    )?
160    .try_into()
161    .map_err(|_| "operation-id-hex must contain exactly 32 bytes")?;
162    Ok(RetirementContext {
163        icp_executable,
164        icp_root: PathBuf::from(icp_root).canonicalize()?,
165        environment,
166        release_build_id: release_build_id.parse()?,
167        coordinator: Principal::from_text(coordinator)?,
168        fleet_subnet_root: Principal::from_text(fleet_subnet_root)?,
169        operation_id,
170    })
171}
172
173fn coordinator_registry(
174    icp: &IcpCli,
175    binding: &ResolvedProtocolBinding,
176    coordinator: Principal,
177) -> Result<FleetRegistry, Box<dyn std::error::Error>> {
178    match query(
179        icp,
180        binding,
181        coordinator,
182        protocol::CANIC_STATUS,
183        &CoordinatorStatusRequest::Registry,
184    )? {
185        CoordinatorStatusResponse::Registry(registry) => Ok(registry),
186        _ => Err("Coordinator returned a differently correlated status response".into()),
187    }
188}
189
190fn query<I, O>(
191    icp: &IcpCli,
192    binding: &ResolvedProtocolBinding,
193    canister: Principal,
194    method: &str,
195    input: &I,
196) -> Result<O, Box<dyn std::error::Error>>
197where
198    I: CandidType,
199    O: CandidType + DeserializeOwned,
200{
201    invoke(icp, binding, canister, method, input, true)
202}
203
204fn call<I, O>(
205    icp: &IcpCli,
206    binding: &ResolvedProtocolBinding,
207    canister: Principal,
208    method: &str,
209    input: &I,
210) -> Result<O, Box<dyn std::error::Error>>
211where
212    I: CandidType,
213    O: CandidType + DeserializeOwned,
214{
215    invoke(icp, binding, canister, method, input, false)
216}
217
218fn invoke<I, O>(
219    icp: &IcpCli,
220    binding: &ResolvedProtocolBinding,
221    canister: Principal,
222    method: &str,
223    input: &I,
224    is_query: bool,
225) -> Result<O, Box<dyn std::error::Error>>
226where
227    I: CandidType,
228    O: CandidType + DeserializeOwned,
229{
230    let bytes = candid::encode_one(input)?;
231    let path = env::temp_dir().join(format!(
232        "canic-empty-root-retirement-{}-{}.bin",
233        std::process::id(),
234        method
235    ));
236    fs::write(&path, bytes)?;
237    let response = if is_query {
238        icp.canister_query_binary_args_output_with_candid(
239            &canister.to_text(),
240            method,
241            &path,
242            Some("json"),
243            Some(binding.candid_path()),
244        )
245    } else {
246        icp.canister_call_binary_args_output_with_candid(
247            &canister.to_text(),
248            method,
249            &path,
250            Some("json"),
251            Some(binding.candid_path()),
252        )
253    };
254    let cleanup = fs::remove_file(path);
255    let response = response?;
256    cleanup?;
257    Ok(decode_json_result_response(&response)?)
258}