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