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