Skip to main content

canic_host/deployment_truth/
plan.rs

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///
12/// LocalDeploymentPlanRequest
13///
14#[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/// Build a local deployment plan from resolved host config and local artifact
26/// observations without querying or mutating IC state.
27#[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}