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