1use super::*;
2use crate::{
3 install_root::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 =
78 local_root_canister_id(request, &fleet_template, &mut unresolved_assumptions);
79 let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
80 let canonical_runtime_config_digest =
81 canonical_runtime_config_assumption(&config, &mut unresolved_assumptions);
82 let deployment_manifest_digest =
83 deployment_manifest_digest_assumption(request, &mut unresolved_assumptions);
84 let artifact_manifest = local_artifact_manifest(request, config);
85 extend_artifact_assumptions(
86 &mut unresolved_assumptions,
87 artifact_manifest.unresolved_artifacts,
88 );
89 let authority_profile = local_authority_profile(request, expected_controllers);
90 let role_artifacts = local_plan_role_artifacts(
91 artifact_manifest.role_artifacts,
92 &request.build_profile,
93 raw_config_sha256.as_ref(),
94 );
95 let expected_canisters = local_expected_canisters(roles, root_canister_id.as_deref());
96 let identity = local_plan_identity(
97 request,
98 PlanIdentityFacts {
99 root_canister_id: root_canister_id.clone(),
100 deployment_manifest_digest,
101 canonical_runtime_config_digest,
102 authority_profile: &authority_profile,
103 expected_canisters: &expected_canisters,
104 role_artifacts: &role_artifacts,
105 expected_pool: &expected_pool,
106 },
107 );
108
109 DeploymentPlanV1 {
110 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
111 plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
112 deployment_identity: identity,
113 trust_domain: TrustDomainV1 {
114 root_trust_anchor: root_canister_id,
115 migration_from: None,
116 },
117 fleet_template,
118 runtime_variant: request.runtime_variant.clone(),
119 authority_profile,
120 role_artifacts,
121 expected_canisters,
122 expected_pool,
123 expected_verifier_readiness: VerifierReadinessExpectationV1 {
124 required: false,
125 expected_role_epochs: Vec::new(),
126 },
127 unresolved_assumptions,
128 }
129}
130
131struct PlanIdentityFacts<'a> {
132 root_canister_id: Option<String>,
133 deployment_manifest_digest: Option<String>,
134 canonical_runtime_config_digest: Option<String>,
135 authority_profile: &'a AuthorityProfileV1,
136 expected_canisters: &'a [ExpectedCanisterV1],
137 role_artifacts: &'a [RoleArtifactV1],
138 expected_pool: &'a [ExpectedPoolCanisterV1],
139}
140
141fn local_artifact_manifest(
142 request: &LocalDeploymentPlanRequest,
143 config: PathBuf,
144) -> RoleArtifactManifestV1 {
145 collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
146 network: request.network.clone(),
147 workspace_root: request.workspace_root.clone(),
148 icp_root: request.icp_root.clone(),
149 config_path: Some(config),
150 })
151}
152
153fn local_plan_identity(
154 request: &LocalDeploymentPlanRequest,
155 facts: PlanIdentityFacts<'_>,
156) -> DeploymentIdentityV1 {
157 local_deployment_identity(
158 request,
159 PlanIdentityInput {
160 root_canister_id: facts.root_canister_id,
161 deployment_manifest_digest: facts.deployment_manifest_digest,
162 canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
163 authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
164 role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
165 artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
166 pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
167 },
168 )
169}
170
171fn local_root_canister_id(
172 request: &LocalDeploymentPlanRequest,
173 fleet_template: &str,
174 assumptions: &mut Vec<DeploymentAssumptionV1>,
175) -> Option<String> {
176 match read_named_fleet_install_state_from_root(
177 &request.icp_root,
178 &request.network,
179 fleet_template,
180 ) {
181 Ok(Some(state)) if state.network == request.network => Some(state.root_canister_id),
182 Ok(Some(state)) => {
183 assumptions.push(assumption(
184 "local_state.root_canister_id",
185 format!(
186 "install state for fleet {fleet_template} has network {}, expected {}",
187 state.network, request.network
188 ),
189 ));
190 None
191 }
192 Ok(None) => {
193 assumptions.push(assumption(
194 "local_state.root_canister_id",
195 format!(
196 "no local install state exists for fleet {fleet_template}; root identity is unknown until install"
197 ),
198 ));
199 None
200 }
201 Err(err) => {
202 assumptions.push(assumption(
203 "local_state.root_canister_id",
204 format!("could not read install state for fleet {fleet_template}: {err}"),
205 ));
206 None
207 }
208 }
209}
210
211struct PlanIdentityInput {
212 root_canister_id: Option<String>,
213 deployment_manifest_digest: Option<String>,
214 canonical_runtime_config_digest: Option<String>,
215 authority_profile_hash: Option<String>,
216 role_topology_hash: Option<String>,
217 artifact_set_digest: Option<String>,
218 pool_identity_set_digest: Option<String>,
219}
220
221fn local_deployment_identity(
222 request: &LocalDeploymentPlanRequest,
223 input: PlanIdentityInput,
224) -> DeploymentIdentityV1 {
225 DeploymentIdentityV1 {
226 deployment_name: request.deployment_name.clone(),
227 network: request.network.clone(),
228 root_principal: input.root_canister_id,
229 authority_profile_hash: input.authority_profile_hash,
230 role_topology_hash: input.role_topology_hash,
231 deployment_manifest_digest: input.deployment_manifest_digest,
232 canonical_runtime_config_digest: input.canonical_runtime_config_digest,
233 role_embedded_config_set_digest: None,
234 artifact_set_digest: input.artifact_set_digest,
235 pool_identity_set_digest: input.pool_identity_set_digest,
236 canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
237 ic_memory_version: None,
238 }
239}
240
241fn local_authority_profile(
242 request: &LocalDeploymentPlanRequest,
243 expected_controllers: Vec<String>,
244) -> AuthorityProfileV1 {
245 AuthorityProfileV1 {
246 profile_id: format!(
247 "local:{}:{}:authority",
248 request.network, request.deployment_name
249 ),
250 expected_controllers,
251 staging_controllers: Vec::new(),
252 emergency_controllers: Vec::new(),
253 }
254}
255
256fn local_expected_canisters(
257 roles: Vec<String>,
258 root_canister_id: Option<&str>,
259) -> Vec<ExpectedCanisterV1> {
260 roles
261 .into_iter()
262 .map(|role| ExpectedCanisterV1 {
263 canister_id: if role == "root" {
264 root_canister_id.map(str::to_string)
265 } else {
266 None
267 },
268 role,
269 control_class: CanisterControlClassV1::DeploymentControlled,
270 })
271 .collect()
272}
273
274fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
275 pools
276 .into_iter()
277 .map(|pool| ExpectedPoolCanisterV1 {
278 pool: pool.pool,
279 canister_id: None,
280 role: Some(pool.canister_role),
281 })
282 .collect()
283}
284
285fn local_plan_role_artifacts(
286 artifacts: Vec<RoleArtifactV1>,
287 build_profile: &str,
288 raw_config_sha256: Option<&String>,
289) -> Vec<RoleArtifactV1> {
290 artifacts
291 .into_iter()
292 .map(|mut artifact| {
293 artifact.build_profile = build_profile.to_string();
294 artifact.raw_config_sha256 = raw_config_sha256.cloned();
295 artifact
296 })
297 .collect()
298}
299
300fn extend_artifact_assumptions(
301 assumptions: &mut Vec<DeploymentAssumptionV1>,
302 gaps: Vec<DeploymentObservationGapV1>,
303) {
304 assumptions.extend(
305 gaps.into_iter()
306 .map(|gap| assumption(gap.key, gap.description)),
307 );
308}
309
310fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
311 DeploymentAssumptionV1 {
312 key: key.into(),
313 description: description.into(),
314 }
315}
316
317fn config_sha256_assumption(
318 path: &std::path::Path,
319 assumptions: &mut Vec<DeploymentAssumptionV1>,
320) -> Option<String> {
321 match file_sha256_hex(path) {
322 Ok(hash) => Some(hash),
323 Err(err) => {
324 assumptions.push(assumption(
325 "local_config.raw_sha256",
326 format!("could not hash config {}: {err}", path.display()),
327 ));
328 None
329 }
330 }
331}
332
333fn canonical_runtime_config_assumption(
334 path: &std::path::Path,
335 assumptions: &mut Vec<DeploymentAssumptionV1>,
336) -> Option<String> {
337 match canonical_runtime_config_sha256_hex(path) {
338 Ok(hash) => Some(hash),
339 Err(err) => {
340 assumptions.push(assumption(
341 "local_config.canonical_runtime_config_sha256",
342 format!(
343 "could not hash canonical runtime config {}: {err}",
344 path.display()
345 ),
346 ));
347 None
348 }
349 }
350}
351
352fn deployment_manifest_digest_assumption(
353 request: &LocalDeploymentPlanRequest,
354 assumptions: &mut Vec<DeploymentAssumptionV1>,
355) -> Option<String> {
356 let mut gaps = Vec::new();
357 let digest =
358 super::observe::release_set_manifest_digest(&request.icp_root, &request.network, &mut gaps);
359 assumptions.extend(
360 gaps.into_iter()
361 .map(|gap| assumption(gap.key, gap.description)),
362 );
363 digest
364}