canic_host/deployment_truth/
plan.rs1use super::*;
2use crate::{
3 install_root::read_named_fleet_install_state_from_root,
4 release_set::{configured_controllers, configured_fleet_name, configured_fleet_roles},
5};
6use std::path::PathBuf;
7
8#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct LocalDeploymentPlanRequest {
13 pub deployment_name: String,
14 pub network: String,
15 pub workspace_root: PathBuf,
16 pub icp_root: PathBuf,
17 pub config_path: Option<PathBuf>,
18 pub runtime_variant: String,
19 pub build_profile: String,
20}
21
22#[must_use]
25pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
26 let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
27 let mut unresolved_assumptions = Vec::new();
28 let fleet_template = configured_fleet_name(&config).unwrap_or_else(|err| {
29 unresolved_assumptions.push(assumption(
30 "local_config.fleet_name",
31 format!(
32 "could not resolve fleet template name from {}: {err}",
33 config.display()
34 ),
35 ));
36 request.deployment_name.clone()
37 });
38 let roles = configured_fleet_roles(&config).unwrap_or_else(|err| {
39 unresolved_assumptions.push(assumption(
40 "local_config.roles",
41 format!(
42 "could not resolve configured roles from {}: {err}",
43 config.display()
44 ),
45 ));
46 Vec::new()
47 });
48 let expected_controllers = configured_controllers(&config).unwrap_or_else(|err| {
49 unresolved_assumptions.push(assumption(
50 "local_config.controllers",
51 format!(
52 "could not resolve configured controllers from {}: {err}",
53 config.display()
54 ),
55 ));
56 Vec::new()
57 });
58 let root_canister_id =
59 local_root_canister_id(request, &fleet_template, &mut unresolved_assumptions);
60 let raw_config_sha256 = config_sha256_assumption(&config, &mut unresolved_assumptions);
61 let artifact_manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
62 network: request.network.clone(),
63 workspace_root: request.workspace_root.clone(),
64 icp_root: request.icp_root.clone(),
65 config_path: Some(config),
66 });
67 unresolved_assumptions.extend(
68 artifact_manifest
69 .unresolved_artifacts
70 .into_iter()
71 .map(|gap| assumption(gap.key, gap.description)),
72 );
73
74 DeploymentPlanV1 {
75 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
76 plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
77 deployment_identity: local_deployment_identity(request, root_canister_id.clone()),
78 trust_domain: TrustDomainV1 {
79 root_trust_anchor: root_canister_id.clone(),
80 migration_from: None,
81 },
82 fleet_template,
83 runtime_variant: request.runtime_variant.clone(),
84 authority_profile: local_authority_profile(request, expected_controllers),
85 role_artifacts: artifact_manifest
86 .role_artifacts
87 .into_iter()
88 .map(|mut artifact| {
89 artifact.build_profile.clone_from(&request.build_profile);
90 artifact.raw_config_sha256.clone_from(&raw_config_sha256);
91 artifact
92 })
93 .collect(),
94 expected_canisters: local_expected_canisters(roles, root_canister_id.as_deref()),
95 expected_pool: Vec::new(),
96 expected_verifier_readiness: VerifierReadinessExpectationV1 {
97 required: false,
98 expected_role_epochs: Vec::new(),
99 },
100 unresolved_assumptions,
101 }
102}
103
104fn local_root_canister_id(
105 request: &LocalDeploymentPlanRequest,
106 fleet_template: &str,
107 assumptions: &mut Vec<DeploymentAssumptionV1>,
108) -> Option<String> {
109 match read_named_fleet_install_state_from_root(
110 &request.icp_root,
111 &request.network,
112 fleet_template,
113 ) {
114 Ok(Some(state)) if state.network == request.network => Some(state.root_canister_id),
115 Ok(Some(state)) => {
116 assumptions.push(assumption(
117 "local_state.root_canister_id",
118 format!(
119 "install state for fleet {fleet_template} has network {}, expected {}",
120 state.network, request.network
121 ),
122 ));
123 None
124 }
125 Ok(None) => {
126 assumptions.push(assumption(
127 "local_state.root_canister_id",
128 format!(
129 "no local install state exists for fleet {fleet_template}; root identity is unknown until install"
130 ),
131 ));
132 None
133 }
134 Err(err) => {
135 assumptions.push(assumption(
136 "local_state.root_canister_id",
137 format!("could not read install state for fleet {fleet_template}: {err}"),
138 ));
139 None
140 }
141 }
142}
143
144fn local_deployment_identity(
145 request: &LocalDeploymentPlanRequest,
146 root_canister_id: Option<String>,
147) -> DeploymentIdentityV1 {
148 DeploymentIdentityV1 {
149 deployment_name: request.deployment_name.clone(),
150 network: request.network.clone(),
151 root_principal: root_canister_id,
152 authority_profile_hash: None,
153 role_topology_hash: None,
154 deployment_manifest_digest: None,
155 canonical_runtime_config_digest: None,
156 role_embedded_config_set_digest: None,
157 artifact_set_digest: None,
158 pool_identity_set_digest: None,
159 canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
160 ic_memory_version: None,
161 }
162}
163
164fn local_authority_profile(
165 request: &LocalDeploymentPlanRequest,
166 expected_controllers: Vec<String>,
167) -> AuthorityProfileV1 {
168 AuthorityProfileV1 {
169 profile_id: format!(
170 "local:{}:{}:authority",
171 request.network, request.deployment_name
172 ),
173 expected_controllers,
174 staging_controllers: Vec::new(),
175 emergency_controllers: Vec::new(),
176 }
177}
178
179fn local_expected_canisters(
180 roles: Vec<String>,
181 root_canister_id: Option<&str>,
182) -> Vec<ExpectedCanisterV1> {
183 roles
184 .into_iter()
185 .map(|role| ExpectedCanisterV1 {
186 canister_id: if role == "root" {
187 root_canister_id.map(str::to_string)
188 } else {
189 None
190 },
191 role,
192 control_class: CanisterControlClassV1::DeploymentControlled,
193 })
194 .collect()
195}
196
197fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
198 DeploymentAssumptionV1 {
199 key: key.into(),
200 description: description.into(),
201 }
202}
203
204fn config_sha256_assumption(
205 path: &std::path::Path,
206 assumptions: &mut Vec<DeploymentAssumptionV1>,
207) -> Option<String> {
208 match file_sha256_hex(path) {
209 Ok(hash) => Some(hash),
210 Err(err) => {
211 assumptions.push(assumption(
212 "local_config.raw_sha256",
213 format!("could not hash config {}: {err}", path.display()),
214 ));
215 None
216 }
217 }
218}