empty_fleet_subnet_root_retirement/
empty_fleet_subnet_root_retirement.rs1use 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}