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