1use super::*;
2use crate::{
3 fleet_catalog::{FleetCatalogEntryV1, read_fleet_catalog_entry_from_root},
4 network::resolve_canonical_network_id_from_root,
5 release_set::{AppConfigSnapshot, ConfiguredPoolExpectation, artifact_root_path},
6};
7use canic_core::ids::CanonicalNetworkId;
8use std::path::{Path, PathBuf};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct LocalDeploymentPlanRequest {
15 pub fleet_name: String,
16 pub app: String,
17 pub environment: String,
18 pub artifact_environment: String,
19 pub workspace_root: PathBuf,
20 pub icp_root: PathBuf,
21 pub config_path: Option<PathBuf>,
22 pub runtime_variant: String,
23 pub build_profile: String,
24}
25
26#[must_use]
29pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
30 let artifact_root = artifact_root_path(&request.icp_root, &request.artifact_environment);
31 build_local_deployment_plan_at_root(request, &artifact_root)
32}
33
34pub fn build_local_deployment_plan_at_root(
35 request: &LocalDeploymentPlanRequest,
36 artifact_root: &Path,
37) -> DeploymentPlanV1 {
38 let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
39 let mut unresolved_assumptions = Vec::new();
40 let (roles, expected_pool) = match AppConfigSnapshot::load(&config) {
41 Ok(snapshot) => {
42 if snapshot.app_id() != request.app {
43 unresolved_assumptions.push(assumption(
44 "local_config.app",
45 format!(
46 "{} declares App {}, not requested App {}",
47 config.display(),
48 snapshot.app_id(),
49 request.app
50 ),
51 ));
52 }
53 (
54 deployment_truth_roles_with_built_in_infrastructure(snapshot.bootstrap_roles()),
55 local_expected_pool(snapshot.pool_expectations()),
56 )
57 }
58 Err(err) => {
59 for (code, subject) in [
60 ("local_config.app", "App identity"),
61 ("local_config.roles", "configured roles"),
62 ("local_config.pools", "configured pool expectations"),
63 ] {
64 unresolved_assumptions.push(assumption(
65 code,
66 format!(
67 "could not resolve {subject} from {}: {err}",
68 config.display()
69 ),
70 ));
71 }
72 (Vec::new(), Vec::new())
73 }
74 };
75 let resolved_fleet = local_fleet_identity(request, &mut unresolved_assumptions);
76 let root_canister_id = None;
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, artifact_root, &mut unresolved_assumptions);
85 let artifact_manifest = local_artifact_manifest(request, artifact_root, config);
86 extend_artifact_assumptions(
87 &mut unresolved_assumptions,
88 artifact_manifest.unresolved_artifacts,
89 );
90 let authority_profile = local_authority_profile(request);
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 resolved_fleet: &resolved_fleet,
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 },
117 runtime_variant: request.runtime_variant.clone(),
118 authority_profile,
119 role_artifacts,
120 expected_canisters,
121 expected_pool,
122 expected_verifier_readiness: VerifierReadinessExpectationV1 {
123 required: false,
124 expected_role_epochs: Vec::new(),
125 },
126 unresolved_assumptions,
127 }
128}
129
130fn local_plan_id(request: &LocalDeploymentPlanRequest) -> String {
131 format!("local:{}:{}:plan", request.environment, request.fleet_name)
132}
133
134struct PlanIdentityFacts<'a> {
135 root_canister_id: Option<String>,
136 resolved_fleet: &'a ResolvedPlanFleet,
137 deployment_manifest_digest: Option<String>,
138 canonical_runtime_config_digest: Option<String>,
139 authority_profile: &'a AuthorityProfileV1,
140 expected_canisters: &'a [ExpectedCanisterV1],
141 role_artifacts: &'a [RoleArtifactV1],
142 expected_pool: &'a [ExpectedPoolCanisterV1],
143}
144
145fn local_artifact_manifest(
146 request: &LocalDeploymentPlanRequest,
147 artifact_root: &Path,
148 config: PathBuf,
149) -> RoleArtifactManifestV1 {
150 collect_local_role_artifact_manifest_at_root(
151 &LocalArtifactManifestRequest {
152 environment: request.environment.clone(),
153 artifact_environment: request.artifact_environment.clone(),
154 workspace_root: request.workspace_root.clone(),
155 icp_root: request.icp_root.clone(),
156 config_path: Some(config),
157 },
158 artifact_root,
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 canonical_network_id: facts.resolved_fleet.canonical_network_id,
171 fleet_id: facts
172 .resolved_fleet
173 .catalog
174 .as_ref()
175 .map(|fleet| fleet.fleet_id),
176 deployment_manifest_digest: facts.deployment_manifest_digest,
177 canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
178 authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
179 role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
180 artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
181 pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
182 },
183 )
184}
185
186struct ResolvedPlanFleet {
187 canonical_network_id: Option<CanonicalNetworkId>,
188 catalog: Option<FleetCatalogEntryV1>,
189}
190
191fn local_fleet_identity(
192 request: &LocalDeploymentPlanRequest,
193 assumptions: &mut Vec<DeploymentAssumptionV1>,
194) -> ResolvedPlanFleet {
195 let canonical_network_id =
196 match resolve_canonical_network_id_from_root(&request.icp_root, &request.environment) {
197 Ok(network) => Some(network),
198 Err(error) => {
199 assumptions.push(assumption(
200 DeploymentAssumptionKindV1::FleetCatalogReadFailed.key(),
201 format!(
202 "could not resolve canonical network identity for {}: {error}",
203 request.environment
204 ),
205 ));
206 None
207 }
208 };
209 let catalog = if canonical_network_id.is_none() {
210 None
211 } else {
212 match read_fleet_catalog_entry_from_root(
213 &request.icp_root,
214 &request.environment,
215 &request.fleet_name,
216 ) {
217 Ok(Some(fleet)) => Some(fleet),
218 Ok(None) => {
219 assumptions.push(assumption(
220 DeploymentAssumptionKindV1::FleetCatalogMissing.key(),
221 format!(
222 "no installed Fleet catalog entry exists for {}; Coordinator identity is unknown until installation completes",
223 request.fleet_name
224 ),
225 ));
226 None
227 }
228 Err(error) => {
229 assumptions.push(assumption(
230 DeploymentAssumptionKindV1::FleetCatalogReadFailed.key(),
231 format!(
232 "could not read Fleet catalog for {}: {error}",
233 request.fleet_name,
234 ),
235 ));
236 None
237 }
238 }
239 };
240 ResolvedPlanFleet {
241 canonical_network_id,
242 catalog,
243 }
244}
245
246struct PlanIdentityInput {
247 canonical_network_id: Option<CanonicalNetworkId>,
248 fleet_id: Option<canic_core::ids::FleetId>,
249 root_canister_id: Option<String>,
250 deployment_manifest_digest: Option<String>,
251 canonical_runtime_config_digest: Option<String>,
252 authority_profile_hash: Option<String>,
253 role_topology_hash: Option<String>,
254 artifact_set_digest: Option<String>,
255 pool_identity_set_digest: Option<String>,
256}
257
258fn local_deployment_identity(
259 request: &LocalDeploymentPlanRequest,
260 input: PlanIdentityInput,
261) -> DeploymentIdentityV1 {
262 DeploymentIdentityV1 {
263 canonical_network_id: input.canonical_network_id,
264 fleet_id: input.fleet_id,
265 fleet_name: request.fleet_name.clone(),
266 app: request.app.clone(),
267 environment: request.environment.clone(),
268 root_principal: input.root_canister_id,
269 authority_profile_hash: input.authority_profile_hash,
270 role_topology_hash: input.role_topology_hash,
271 deployment_manifest_digest: input.deployment_manifest_digest,
272 canonical_runtime_config_digest: input.canonical_runtime_config_digest,
273 role_embedded_config_set_digest: None,
274 artifact_set_digest: input.artifact_set_digest,
275 pool_identity_set_digest: input.pool_identity_set_digest,
276 canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
277 ic_memory_version: None,
278 }
279}
280
281fn local_authority_profile(request: &LocalDeploymentPlanRequest) -> AuthorityProfileV1 {
282 AuthorityProfileV1 {
285 profile_id: format!(
286 "local:{}:{}:authority",
287 request.environment, request.fleet_name
288 ),
289 expected_controllers: Vec::new(),
290 staging_controllers: Vec::new(),
291 emergency_controllers: Vec::new(),
292 }
293}
294
295fn local_expected_canisters(
296 roles: Vec<String>,
297 root_canister_id: Option<&str>,
298) -> Vec<ExpectedCanisterV1> {
299 roles
300 .into_iter()
301 .map(|role| ExpectedCanisterV1 {
302 canister_id: if role == "root" {
303 root_canister_id.map(str::to_string)
304 } else {
305 None
306 },
307 role,
308 control_class: CanisterControlClassV1::DeploymentControlled,
309 })
310 .collect()
311}
312
313fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
314 pools
315 .into_iter()
316 .map(|pool| ExpectedPoolCanisterV1 {
317 pool: pool.pool,
318 canister_id: None,
319 role: Some(pool.canister_role),
320 })
321 .collect()
322}
323
324fn local_plan_role_artifacts(
325 artifacts: Vec<RoleArtifactV1>,
326 build_profile: &str,
327 raw_config_sha256: Option<&String>,
328) -> Vec<RoleArtifactV1> {
329 artifacts
330 .into_iter()
331 .map(|mut artifact| {
332 artifact.build_profile = build_profile.to_string();
333 artifact.raw_config_sha256 = raw_config_sha256.cloned();
334 artifact
335 })
336 .collect()
337}
338
339fn extend_artifact_assumptions(
340 assumptions: &mut Vec<DeploymentAssumptionV1>,
341 gaps: Vec<DeploymentObservationGapV1>,
342) {
343 assumptions.extend(
344 gaps.into_iter()
345 .map(|gap| assumption(gap.key, gap.description)),
346 );
347}
348
349fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
350 DeploymentAssumptionV1 {
351 key: key.into(),
352 description: description.into(),
353 }
354}
355
356fn config_sha256_assumption(
357 path: &std::path::Path,
358 assumptions: &mut Vec<DeploymentAssumptionV1>,
359) -> Option<String> {
360 match file_sha256_hex(path) {
361 Ok(hash) => Some(hash),
362 Err(err) => {
363 assumptions.push(assumption(
364 "local_config.raw_sha256",
365 format!("could not hash config {}: {err}", path.display()),
366 ));
367 None
368 }
369 }
370}
371
372fn canonical_runtime_config_assumption(
373 path: &std::path::Path,
374 assumptions: &mut Vec<DeploymentAssumptionV1>,
375) -> Option<String> {
376 match canonical_runtime_config_sha256_hex(path) {
377 Ok(hash) => Some(hash),
378 Err(err) => {
379 assumptions.push(assumption(
380 "local_config.canonical_runtime_config_sha256",
381 format!(
382 "could not hash canonical runtime config {}: {err}",
383 path.display()
384 ),
385 ));
386 None
387 }
388 }
389}
390
391fn deployment_manifest_digest_assumption(
392 request: &LocalDeploymentPlanRequest,
393 artifact_root: &Path,
394 assumptions: &mut Vec<DeploymentAssumptionV1>,
395) -> Option<String> {
396 let mut gaps = Vec::new();
397 let digest =
398 super::observe::release_set_manifest_digest(&request.icp_root, artifact_root, &mut gaps);
399 assumptions.extend(
400 gaps.into_iter()
401 .map(|gap| assumption(gap.key, gap.description)),
402 );
403 digest
404}