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