Skip to main content

canic_host/deployment_truth/
plan.rs

1use super::*;
2use crate::{
3    install_root::{
4        InstallStateError, RootVerificationStatus, read_named_deployment_install_state_from_root,
5    },
6    release_set::{
7        ConfiguredPoolExpectation, configured_bootstrap_roles, configured_controllers,
8        configured_fleet_name, configured_pool_expectations,
9    },
10};
11use std::path::PathBuf;
12
13///
14/// LocalDeploymentPlanRequest
15///
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct LocalDeploymentPlanRequest {
18    pub deployment_name: String,
19    pub network: String,
20    pub workspace_root: PathBuf,
21    pub icp_root: PathBuf,
22    pub config_path: Option<PathBuf>,
23    pub runtime_variant: String,
24    pub build_profile: String,
25}
26
27/// Build a local deployment plan from resolved host config and local artifact
28/// observations without querying or mutating IC state.
29#[must_use]
30pub fn build_local_deployment_plan(request: &LocalDeploymentPlanRequest) -> DeploymentPlanV1 {
31    let config = deployment_config_path(&request.workspace_root, request.config_path.as_deref());
32    let mut unresolved_assumptions = Vec::new();
33    let fleet_template = configured_fleet_name(&config).unwrap_or_else(|err| {
34        unresolved_assumptions.push(assumption(
35            "local_config.fleet_name",
36            format!(
37                "could not resolve fleet template name from {}: {err}",
38                config.display()
39            ),
40        ));
41        request.deployment_name.clone()
42    });
43    let roles = configured_bootstrap_roles(&config).map_or_else(
44        |err| {
45            unresolved_assumptions.push(assumption(
46                "local_config.roles",
47                format!(
48                    "could not resolve configured roles from {}: {err}",
49                    config.display()
50                ),
51            ));
52            Vec::new()
53        },
54        deployment_truth_roles_with_implicit_wasm_store,
55    );
56    let expected_controllers = configured_controllers(&config).unwrap_or_else(|err| {
57        unresolved_assumptions.push(assumption(
58            "local_config.controllers",
59            format!(
60                "could not resolve configured controllers from {}: {err}",
61                config.display()
62            ),
63        ));
64        Vec::new()
65    });
66    let expected_pool = configured_pool_expectations(&config).map_or_else(
67        |err| {
68            unresolved_assumptions.push(assumption(
69                "local_config.pools",
70                format!(
71                    "could not resolve configured pool expectations from {}: {err}",
72                    config.display()
73                ),
74            ));
75            Vec::new()
76        },
77        local_expected_pool,
78    );
79    let root_canister_id = local_root_canister_id(request, &mut unresolved_assumptions);
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, &mut unresolved_assumptions);
85    let artifact_manifest = local_artifact_manifest(request, config);
86    extend_artifact_assumptions(
87        &mut unresolved_assumptions,
88        artifact_manifest.unresolved_artifacts,
89    );
90    let authority_profile = local_authority_profile(request, expected_controllers);
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            deployment_manifest_digest,
102            canonical_runtime_config_digest,
103            authority_profile: &authority_profile,
104            expected_canisters: &expected_canisters,
105            role_artifacts: &role_artifacts,
106            expected_pool: &expected_pool,
107        },
108    );
109
110    DeploymentPlanV1 {
111        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
112        plan_id: format!("local:{}:{}:plan", request.network, request.deployment_name),
113        deployment_identity: identity,
114        trust_domain: TrustDomainV1 {
115            root_trust_anchor: root_canister_id,
116            migration_from: None,
117        },
118        fleet_template,
119        runtime_variant: request.runtime_variant.clone(),
120        authority_profile,
121        role_artifacts,
122        expected_canisters,
123        expected_pool,
124        expected_verifier_readiness: VerifierReadinessExpectationV1 {
125            required: false,
126            expected_role_epochs: Vec::new(),
127        },
128        unresolved_assumptions,
129    }
130}
131
132struct PlanIdentityFacts<'a> {
133    root_canister_id: Option<String>,
134    deployment_manifest_digest: Option<String>,
135    canonical_runtime_config_digest: Option<String>,
136    authority_profile: &'a AuthorityProfileV1,
137    expected_canisters: &'a [ExpectedCanisterV1],
138    role_artifacts: &'a [RoleArtifactV1],
139    expected_pool: &'a [ExpectedPoolCanisterV1],
140}
141
142fn local_artifact_manifest(
143    request: &LocalDeploymentPlanRequest,
144    config: PathBuf,
145) -> RoleArtifactManifestV1 {
146    collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
147        network: request.network.clone(),
148        workspace_root: request.workspace_root.clone(),
149        icp_root: request.icp_root.clone(),
150        config_path: Some(config),
151    })
152}
153
154fn local_plan_identity(
155    request: &LocalDeploymentPlanRequest,
156    facts: PlanIdentityFacts<'_>,
157) -> DeploymentIdentityV1 {
158    local_deployment_identity(
159        request,
160        PlanIdentityInput {
161            root_canister_id: facts.root_canister_id,
162            deployment_manifest_digest: facts.deployment_manifest_digest,
163            canonical_runtime_config_digest: facts.canonical_runtime_config_digest,
164            authority_profile_hash: Some(stable_json_sha256_hex(facts.authority_profile)),
165            role_topology_hash: Some(stable_json_sha256_hex(&facts.expected_canisters)),
166            artifact_set_digest: Some(stable_json_sha256_hex(&facts.role_artifacts)),
167            pool_identity_set_digest: Some(stable_json_sha256_hex(&facts.expected_pool)),
168        },
169    )
170}
171
172fn local_root_canister_id(
173    request: &LocalDeploymentPlanRequest,
174    assumptions: &mut Vec<DeploymentAssumptionV1>,
175) -> Option<String> {
176    match read_named_deployment_install_state_from_root(
177        &request.icp_root,
178        &request.network,
179        &request.deployment_name,
180    ) {
181        Ok(Some(state)) if state.root_verification == RootVerificationStatus::Verified => {
182            Some(state.root_canister_id)
183        }
184        Ok(Some(state)) => {
185            assumptions.push(assumption(
186                "local_state.unverified_root_canister_id",
187                format!(
188                    "deployment state for {} records root {}, but root verification is {:?}; run deploy check/verification before mutation authority is trusted",
189                    request.deployment_name, state.root_canister_id, state.root_verification
190                ),
191            ));
192            None
193        }
194        Err(InstallStateError::NetworkMismatch {
195            state_network,
196            requested_network,
197        }) => {
198            assumptions.push(assumption(
199                DeploymentAssumptionKindV1::LocalStateNetworkMismatch.key(),
200                format!(
201                    "deployment state for {} has network {}, expected {}",
202                    request.deployment_name, state_network, requested_network
203                ),
204            ));
205            None
206        }
207        Ok(None) => {
208            assumptions.push(assumption(
209                DeploymentAssumptionKindV1::LocalStateMissing.key(),
210                format!(
211                    "no local deployment state exists for {}; root identity is unknown until install or explicit deploy register with --allow-unverified",
212                    request.deployment_name
213                ),
214            ));
215            None
216        }
217        Err(err) => {
218            assumptions.push(assumption(
219                DeploymentAssumptionKindV1::LocalStateReadFailed.key(),
220                format!(
221                    "could not read deployment state for {}: {err}",
222                    request.deployment_name
223                ),
224            ));
225            None
226        }
227    }
228}
229
230struct PlanIdentityInput {
231    root_canister_id: Option<String>,
232    deployment_manifest_digest: Option<String>,
233    canonical_runtime_config_digest: Option<String>,
234    authority_profile_hash: Option<String>,
235    role_topology_hash: Option<String>,
236    artifact_set_digest: Option<String>,
237    pool_identity_set_digest: Option<String>,
238}
239
240fn local_deployment_identity(
241    request: &LocalDeploymentPlanRequest,
242    input: PlanIdentityInput,
243) -> DeploymentIdentityV1 {
244    DeploymentIdentityV1 {
245        deployment_name: request.deployment_name.clone(),
246        network: request.network.clone(),
247        root_principal: input.root_canister_id,
248        authority_profile_hash: input.authority_profile_hash,
249        role_topology_hash: input.role_topology_hash,
250        deployment_manifest_digest: input.deployment_manifest_digest,
251        canonical_runtime_config_digest: input.canonical_runtime_config_digest,
252        role_embedded_config_set_digest: None,
253        artifact_set_digest: input.artifact_set_digest,
254        pool_identity_set_digest: input.pool_identity_set_digest,
255        canic_version: Some(env!("CARGO_PKG_VERSION").to_string()),
256        ic_memory_version: None,
257    }
258}
259
260fn local_authority_profile(
261    request: &LocalDeploymentPlanRequest,
262    expected_controllers: Vec<String>,
263) -> AuthorityProfileV1 {
264    AuthorityProfileV1 {
265        profile_id: format!(
266            "local:{}:{}:authority",
267            request.network, request.deployment_name
268        ),
269        expected_controllers,
270        staging_controllers: Vec::new(),
271        emergency_controllers: Vec::new(),
272    }
273}
274
275fn local_expected_canisters(
276    roles: Vec<String>,
277    root_canister_id: Option<&str>,
278) -> Vec<ExpectedCanisterV1> {
279    roles
280        .into_iter()
281        .map(|role| ExpectedCanisterV1 {
282            canister_id: if role == "root" {
283                root_canister_id.map(str::to_string)
284            } else {
285                None
286            },
287            role,
288            control_class: CanisterControlClassV1::DeploymentControlled,
289        })
290        .collect()
291}
292
293fn local_expected_pool(pools: Vec<ConfiguredPoolExpectation>) -> Vec<ExpectedPoolCanisterV1> {
294    pools
295        .into_iter()
296        .map(|pool| ExpectedPoolCanisterV1 {
297            pool: pool.pool,
298            canister_id: None,
299            role: Some(pool.canister_role),
300        })
301        .collect()
302}
303
304fn local_plan_role_artifacts(
305    artifacts: Vec<RoleArtifactV1>,
306    build_profile: &str,
307    raw_config_sha256: Option<&String>,
308) -> Vec<RoleArtifactV1> {
309    artifacts
310        .into_iter()
311        .map(|mut artifact| {
312            artifact.build_profile = build_profile.to_string();
313            artifact.raw_config_sha256 = raw_config_sha256.cloned();
314            artifact
315        })
316        .collect()
317}
318
319fn extend_artifact_assumptions(
320    assumptions: &mut Vec<DeploymentAssumptionV1>,
321    gaps: Vec<DeploymentObservationGapV1>,
322) {
323    assumptions.extend(
324        gaps.into_iter()
325            .map(|gap| assumption(gap.key, gap.description)),
326    );
327}
328
329fn assumption(key: impl Into<String>, description: impl Into<String>) -> DeploymentAssumptionV1 {
330    DeploymentAssumptionV1 {
331        key: key.into(),
332        description: description.into(),
333    }
334}
335
336fn config_sha256_assumption(
337    path: &std::path::Path,
338    assumptions: &mut Vec<DeploymentAssumptionV1>,
339) -> Option<String> {
340    match file_sha256_hex(path) {
341        Ok(hash) => Some(hash),
342        Err(err) => {
343            assumptions.push(assumption(
344                "local_config.raw_sha256",
345                format!("could not hash config {}: {err}", path.display()),
346            ));
347            None
348        }
349    }
350}
351
352fn canonical_runtime_config_assumption(
353    path: &std::path::Path,
354    assumptions: &mut Vec<DeploymentAssumptionV1>,
355) -> Option<String> {
356    match canonical_runtime_config_sha256_hex(path) {
357        Ok(hash) => Some(hash),
358        Err(err) => {
359            assumptions.push(assumption(
360                "local_config.canonical_runtime_config_sha256",
361                format!(
362                    "could not hash canonical runtime config {}: {err}",
363                    path.display()
364                ),
365            ));
366            None
367        }
368    }
369}
370
371fn deployment_manifest_digest_assumption(
372    request: &LocalDeploymentPlanRequest,
373    assumptions: &mut Vec<DeploymentAssumptionV1>,
374) -> Option<String> {
375    let mut gaps = Vec::new();
376    let digest =
377        super::observe::release_set_manifest_digest(&request.icp_root, &request.network, &mut gaps);
378    assumptions.extend(
379        gaps.into_iter()
380            .map(|gap| assumption(gap.key, gap.description)),
381    );
382    digest
383}