1use super::*;
2use crate::{
3 install_root::{RootVerificationStatus, read_named_fleet_install_state_from_root},
4 release_set::{
5 ConfiguredPoolExpectation, configured_controllers, configured_fleet_name,
6 configured_fleet_roles, configured_pool_expectations,
7 },
8};
9use std::path::PathBuf;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct LocalDeploymentPlanRequest {
16 pub deployment_name: String,
17 pub network: String,
18 pub workspace_root: PathBuf,
19 pub icp_root: PathBuf,
20 pub config_path: Option<PathBuf>,
21 pub runtime_variant: String,
22 pub build_profile: String,
23}
24
25#[must_use]
28pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
29 let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
30 let mut unresolved_assumptions = Vec::new();
31 let fleet_template = configured_fleet_name(&config).unwrap_or_else(|err| {
32 unresolved_assumptions.push(assumption(
33 "local_config.fleet_name",
34 format!(
35 "could not resolve fleet template name from {}: {err}",
36 config.display()
37 ),
38 ));
39 request.deployment_name.clone()
40 });
41 let roles = configured_fleet_roles(&config).map_or_else(
42 |err| {
43 unresolved_assumptions.push(assumption(
44 "local_config.roles",
45 format!(
46 "could not resolve configured roles from {}: {err}",
47 config.display()
48 ),
49 ));
50 Vec::new()
51 },
52 deployment_truth_roles_with_implicit_wasm_store,
53 );
54 let expected_controllers = configured_controllers(&config).unwrap_or_else(|err| {
55 unresolved_assumptions.push(assumption(
56 "local_config.controllers",
57 format!(
58 "could not resolve configured controllers from {}: {err}",
59 config.display()
60 ),
61 ));
62 Vec::new()
63 });
64 let expected_pool = configured_pool_expectations(&config).map_or_else(
65 |err| {
66 unresolved_assumptions.push(assumption(
67 "local_config.pools",
68 format!(
69 "could not resolve configured pool expectations from {}: {err}",
70 config.display()
71 ),
72 ));
73 Vec::new()
74 },
75 local_expected_pool,
76 );
77 let root_canister_id = local_root_canister_id(request, &mut unresolved_assumptions);
78 let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
79 let canonical_runtime_config_digest =
80 canonical_runtime_config_assumption(&config, &mut unresolved_assumptions);
81 let deployment_manifest_digest =
82 deployment_manifest_digest_assumption(request, &mut unresolved_assumptions);
83 let artifact_manifest = local_artifact_manifest(request, config);
84 extend_artifact_assumptions(
85 &mut unresolved_assumptions,
86 artifact_manifest.unresolved_artifacts,
87 );
88 let authority_profile = local_authority_profile(request, expected_controllers);
89 let role_artifacts = local_plan_role_artifacts(
90 artifact_manifest.role_artifacts,
91 &request.build_profile,
92 raw_config_sha256.as_ref(),
93 );
94 let expected_canisters = local_expected_canisters(roles, root_canister_id.as_deref());
95 let identity = local_plan_identity(
96 request,
97 PlanIdentityFacts {
98 root_canister_id: root_canister_id.clone(),
99 deployment_manifest_digest,
100 canonical_runtime_config_digest,
101 authority_profile: &authority_profile,
102 expected_canisters: &expected_canisters,
103 role_artifacts: &role_artifacts,
104 expected_pool: &expected_pool,
105 },
106 );
107
108 DeploymentPlanV1 {
109 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
110 plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
111 deployment_identity: identity,
112 trust_domain: TrustDomainV1 {
113 root_trust_anchor: root_canister_id,
114 migration_from: None,
115 },
116 fleet_template,
117 runtime_variant: request.runtime_variant.clone(),
118 authority_profile,
119 role_artifacts,
120 expected_canisters,
121 expected_pool,
122 expected_verifier_readiness: VerifierReadinessExpectationV1 {
123 required: false,
124 expected_role_epochs: Vec::new(),
125 },
126 unresolved_assumptions,
127 }
128}
129
130struct PlanIdentityFacts<'a> {
131 root_canister_id: Option<String>,
132 deployment_manifest_digest: Option<String>,
133 canonical_runtime_config_digest: Option<String>,
134 authority_profile: &'a AuthorityProfileV1,
135 expected_canisters: &'a [ExpectedCanisterV1],
136 role_artifacts: &'a [RoleArtifactV1],
137 expected_pool: &'a [ExpectedPoolCanisterV1],
138}
139
140fn local_artifact_manifest(
141 request: &LocalDeploymentPlanRequest,
142 config: PathBuf,
143) -> RoleArtifactManifestV1 {
144 collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
145 network: request.network.clone(),
146 workspace_root: request.workspace_root.clone(),
147 icp_root: request.icp_root.clone(),
148 config_path: Some(config),
149 })
150}
151
152fn local_plan_identity(
153 request: &LocalDeploymentPlanRequest,
154 facts: PlanIdentityFacts<'_>,
155) -> DeploymentIdentityV1 {
156 local_deployment_identity(
157 request,
158 PlanIdentityInput {
159 root_canister_id: facts.root_canister_id,
160 deployment_manifest_digest: facts.deployment_manifest_digest,
161 canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
162 authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
163 role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
164 artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
165 pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
166 },
167 )
168}
169
170fn local_root_canister_id(
171 request: &LocalDeploymentPlanRequest,
172 assumptions: &mut Vec<DeploymentAssumptionV1>,
173) -> Option<String> {
174 match read_named_fleet_install_state_from_root(
175 &request.icp_root,
176 &request.network,
177 &request.deployment_name,
178 ) {
179 Ok(Some(state))
180 if state.network == request.network
181 && state.root_verification == RootVerificationStatus::Verified =>
182 {
183 Some(state.root_canister_id)
184 }
185 Ok(Some(state)) if state.network == request.network => {
186 assumptions.push(assumption(
187 "local_state.root_canister_id",
188 format!(
189 "deployment state for {} records root {}, but root verification is {:?}; run deploy check/verification before mutation authority is trusted",
190 request.deployment_name, state.root_canister_id, state.root_verification
191 ),
192 ));
193 None
194 }
195 Ok(Some(state)) => {
196 assumptions.push(assumption(
197 "local_state.root_canister_id",
198 format!(
199 "deployment state for {} has network {}, expected {}",
200 request.deployment_name, state.network, request.network
201 ),
202 ));
203 None
204 }
205 Ok(None) => {
206 assumptions.push(assumption(
207 "local_state.root_canister_id",
208 format!(
209 "no local deployment state exists for {}; root identity is unknown until install or deploy register",
210 request.deployment_name
211 ),
212 ));
213 None
214 }
215 Err(err) => {
216 assumptions.push(assumption(
217 "local_state.root_canister_id",
218 format!(
219 "could not read deployment state for {}: {err}",
220 request.deployment_name
221 ),
222 ));
223 None
224 }
225 }
226}
227
228struct PlanIdentityInput {
229 root_canister_id: Option<String>,
230 deployment_manifest_digest: Option<String>,
231 canonical_runtime_config_digest: Option<String>,
232 authority_profile_hash: Option<String>,
233 role_topology_hash: Option<String>,
234 artifact_set_digest: Option<String>,
235 pool_identity_set_digest: Option<String>,
236}
237
238fn local_deployment_identity(
239 request: &LocalDeploymentPlanRequest,
240 input: PlanIdentityInput,
241) -> DeploymentIdentityV1 {
242 DeploymentIdentityV1 {
243 deployment_name: request.deployment_name.clone(),
244 network: request.network.clone(),
245 root_principal: input.root_canister_id,
246 authority_profile_hash: input.authority_profile_hash,
247 role_topology_hash: input.role_topology_hash,
248 deployment_manifest_digest: input.deployment_manifest_digest,
249 canonical_runtime_config_digest: input.canonical_runtime_config_digest,
250 role_embedded_config_set_digest: None,
251 artifact_set_digest: input.artifact_set_digest,
252 pool_identity_set_digest: input.pool_identity_set_digest,
253 canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
254 ic_memory_version: None,
255 }
256}
257
258fn local_authority_profile(
259 request: &LocalDeploymentPlanRequest,
260 expected_controllers: Vec<String>,
261) -> AuthorityProfileV1 {
262 AuthorityProfileV1 {
263 profile_id: format!(
264 "local:{}:{}:authority",
265 request.network, request.deployment_name
266 ),
267 expected_controllers,
268 staging_controllers: Vec::new(),
269 emergency_controllers: Vec::new(),
270 }
271}
272
273fn local_expected_canisters(
274 roles: Vec<String>,
275 root_canister_id: Option<&str>,
276) -> Vec<ExpectedCanisterV1> {
277 roles
278 .into_iter()
279 .map(|role| ExpectedCanisterV1 {
280 canister_id: if role == "root" {
281 root_canister_id.map(str::to_string)
282 } else {
283 None
284 },
285 role,
286 control_class: CanisterControlClassV1::DeploymentControlled,
287 })
288 .collect()
289}
290
291fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
292 pools
293 .into_iter()
294 .map(|pool| ExpectedPoolCanisterV1 {
295 pool: pool.pool,
296 canister_id: None,
297 role: Some(pool.canister_role),
298 })
299 .collect()
300}
301
302fn local_plan_role_artifacts(
303 artifacts: Vec<RoleArtifactV1>,
304 build_profile: &str,
305 raw_config_sha256: Option<&String>,
306) -> Vec<RoleArtifactV1> {
307 artifacts
308 .into_iter()
309 .map(|mut artifact| {
310 artifact.build_profile = build_profile.to_string();
311 artifact.raw_config_sha256 = raw_config_sha256.cloned();
312 artifact
313 })
314 .collect()
315}
316
317fn extend_artifact_assumptions(
318 assumptions: &mut Vec<DeploymentAssumptionV1>,
319 gaps: Vec<DeploymentObservationGapV1>,
320) {
321 assumptions.extend(
322 gaps.into_iter()
323 .map(|gap| assumption(gap.key, gap.description)),
324 );
325}
326
327fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
328 DeploymentAssumptionV1 {
329 key: key.into(),
330 description: description.into(),
331 }
332}
333
334fn config_sha256_assumption(
335 path: &std::path::Path,
336 assumptions: &mut Vec<DeploymentAssumptionV1>,
337) -> Option<String> {
338 match file_sha256_hex(path) {
339 Ok(hash) => Some(hash),
340 Err(err) => {
341 assumptions.push(assumption(
342 "local_config.raw_sha256",
343 format!("could not hash config {}: {err}", path.display()),
344 ));
345 None
346 }
347 }
348}
349
350fn canonical_runtime_config_assumption(
351 path: &std::path::Path,
352 assumptions: &mut Vec<DeploymentAssumptionV1>,
353) -> Option<String> {
354 match canonical_runtime_config_sha256_hex(path) {
355 Ok(hash) => Some(hash),
356 Err(err) => {
357 assumptions.push(assumption(
358 "local_config.canonical_runtime_config_sha256",
359 format!(
360 "could not hash canonical runtime config {}: {err}",
361 path.display()
362 ),
363 ));
364 None
365 }
366 }
367}
368
369fn deployment_manifest_digest_assumption(
370 request: &LocalDeploymentPlanRequest,
371 assumptions: &mut Vec<DeploymentAssumptionV1>,
372) -> Option<String> {
373 let mut gaps = Vec::new();
374 let digest =
375 super::observe::release_set_manifest_digest(&request.icp_root, &request.network, &mut gaps);
376 assumptions.extend(
377 gaps.into_iter()
378 .map(|gap| assumption(gap.key, gap.description)),
379 );
380 digest
381}